Manifest Reference
manifest.toml describes your driver to a controller. It is the only required file in a
package, and it is checked against the real proxy contracts before the driver is ever
installed — so a typo'd capability, or a connection pointing at a proxy that does not exist,
fails with a list at build time rather than at 9pm in someone's living room.
junodrv check .
The package
A .junodrv is a zip:
manifest.toml required — or manifests/*.toml for several drivers
commands.toml } exactly one kind, matching [driver] runtime
driver.wasm } one module, every platform
driver.py }
driver-macos-aarch64.dylib } native only: one file per platform, architecture in the
driver-linux-x86_64.so } name, because an x86-64 and an aarch64 .so are both .so
driver-linux-aarch64.so }
docs/README.md shown in the controller's driver pane — markdown
icons/128.png optional; 128 / 256 / 512
languages/*.po optional
One driver lives in manifest.toml. Several live in manifests/ — all of them, so there is
one place to look and no file privileged by where it sits.
[driver]
[driver]
id = "sony.bravia.ip" # globally unique, reverse-dotted, permanent
name = "Sony Bravia (IP)"
manufacturer = "Sony"
version = "1.2.0" # semver
runtime = "wasm"
api = 1 # host ABI version
min_core = "0.4.0" # optional
| Field | Required | Meaning |
|---|---|---|
id | required | Permanent. Lowercase letters, digits, . and _. Reverse-dotted by convention, and the registry enforces that for anything certified. |
name | required | What a person sees |
manufacturer | Groups the catalog and the device list | |
version | required | Semver. How you ship an update. |
runtime | required | See below |
api | default 1 | Host ABI version this driver was written against |
min_core | Lowest core version that can run it, as a semver requirement | |
primary | default false | In a multi-driver package, the one that names the artifact |
parent | Driver id of the bridge these devices live behind | |
product | What a catalog calls the whole package, when that is not this driver's name | |
kind | What the product is, as a proxy name. Groups the catalog and picks its icon. | |
variant_of | Driver id of the sibling this is another way of reaching | |
log_level | How much of what this driver says is kept — and that an installer may change it |
id is permanent. Changing it orphans every existing binding in every project that uses the
driver; changing version is how you ship an update.
log_level is the level your driver is worth when nothing is wrong — almost always
info, and debug only for something whose ordinary operation is what somebody is trying to
see. Setting it is also how you say the level may be changed: a driver that declares one gets a
control on the controller's Logs page, and a driver that declares nothing sits at info with no
control, because a switch a driver ignores is worse than no switch.
Everything your driver says through HostCall::Log is held to it. So is anything you print —
a println! reaches the same log, at debug, since it was not addressed to anybody and carries
no level of its own. What it does not govern is the controller's own account of your device:
that it stopped answering, that a command failed. Turning a driver down must not be the way to
stop hearing that its hardware is broken.
min_core is not free. Every bump is a set of houses that stop receiving your updates, and a
controller shows why a newer build is being withheld rather than hiding it — so raise it only
when you actually depend on something newer.
Runtimes
runtime | What ships in the package | |
|---|---|---|
declarative | A commands.toml, and nothing else | Runs |
wasm | One driver.wasm, for every platform. Sandboxed. | Runs |
native | A compiled library per platform. Not sandboxed — it runs in-process with the controller's privileges. | Runs |
adapter | A whole protocol stack in its own process. Needs an [adapter] table. | Runs |
python | A driver.py | Accepted by this format, refused by the controller |
builtin | Nothing — core registers the driver itself | A package can never carry one |
See Runtimes for how to choose, and for what the code on each side looks like.
parent
[driver]
id = "signify.hue.bulb"
name = "Hue Bulb"
version = "1.0.0"
runtime = "wasm"
parent = "signify.hue.bridge"
A child inherits its parent's properties, so a Hue bulb does not carry its own copy of the bridge address — it reads the one the bridge holds. That is the whole point: a bridge that moves to a new IP is edited once.
It is also what makes the catalog show a product the way it is actually installed — the bridge first, its devices under it — rather than a reader guessing the hierarchy out of id prefixes and getting it wrong.
product and kind
A catalog lists products. A manifest describes drivers, and those are not the same thing:
[driver]
id = "signify.hue.bridge"
name = "Philips Hue Bridge"
version = "1.4.0"
runtime = "wasm"
product = "Philips Hue"
kind = "light"
Philips Hue Bridge is the right name for the driver and the wrong one for the shelf — nobody
buys a bridge. bridge is the right proxy for what it implements and the wrong picture for what
it is: filed by its own proxies, the whole Hue family lands under System with a router beside it,
which is not where anyone goes looking for bulbs.
Both are read only on the driver a package leads with — the one primary names, or the
bridge its children point at. On any other they say nothing.
Both fall back, so most packages need neither:
productfalls back to the lead driver'sname, and the controller then trims the words a package's drivers all agree on: seven manifests beginningPhilips Hue …producePhilips Huewithout anybody writing it down. Declare it when they disagree — TP-Link's package is aTP-Link Accountand aTapo Dimmer Switch, which agree on nothing.kindfalls back to the proxy the driver leads with. That is right for anything that is the thing it controls — a television, a receiver, a thermostat — and wrong for every hub.
kind is checked against the proxy registry, so a typo fails junodrv check rather than filing
your product under a shelf nobody can find.
It is one shelf per kind, not per category: a television and a streamer are both av and are
not the same thing to go looking for. Only the kinds a package's drivers lead with count. A
Hisense declares tv and media_player on one driver and is a television — somebody bought a
set, not a set and a streamer — so its second proxy puts it on no second shelf. A package whose
drivers lead with different kinds is genuinely both, and appears under each.
variant_of
The same hardware reached a different way is one product, not two:
[driver]
id = "apple.tv.ir"
name = "Apple TV (IR)"
version = "1.0.0"
runtime = "declarative"
variant_of = "apple.tv"
An Apple TV is a native driver over its Companion link and a commands.toml of IR codes over an
emitter. Listed as siblings, a catalog puts two nearly identical rows in front of somebody before
they know there is a choice to make. With variant_of there is one row, and the choice happens
while it is being added — which is where it matters, because the answer changes the setup that
follows: one flow pairs and shows a code on the television, the other asks which emitter port is
pointed at the box.
Nothing is asked when the device was discovered: something heard over mDNS was heard on the network, so the network driver is the one that gets set up. A package can carry more than one runtime for exactly this reason — see the package.
Nothing is asked when the two are reached the same way, either. Having a variant is not what
makes something a question; being reached differently is, and a controller works that out from
what each driver already declares — its [[control]] kinds if it has any, else its
[[control]] kinds. Roku is the case: roku.player and roku.tv are both roku:ecp on port
8060, and what differs between them is whether the box has a screen, which the setup flow reads
out of /query/device-info. So they are one row that nobody is asked about — and because they
lead with different proxies, one that appears under both Media Players and TVs, drawn as
whichever shelf it was found on.
It must name a driver in the same package. Pointing anywhere else is refused when the package is read, because a variant folded into a product that never arrives is hardware nothing can reach and nothing explains.
[[proxy]]
Which device classes you implement. One block per instance — an eight-relay board has eight, a TV with a built-in streamer has two of different types.
[[proxy]]
id = 1
type = "tv"
name = "Living Room TV" # optional; defaults to the driver name
primary = true # optional; the one the UI leads with
capabilities = { has_discrete_power = true, has_discrete_input = true, warmup_ms = 4000 }
A TOML inline table has to fit on one line. Where there are more capabilities than fit, use a sub-table instead — the two forms parse to the same thing:
[[proxy]]
id = 1
type = "tv"
[proxy.capabilities]
has_discrete_power = true
has_discrete_input = true
has_volume = false
warmup_ms = 4000
id is yours, stable forever, and referenced by control, connection, and every command
callback. Start at 1.
capabilities is the important field. Every key must exist in that proxy's contract — see the
Proxy Reference — and anything you omit takes the documented default. Core
computes your callable command set from this, and nothing downstream can call what you did
not declare.
Note the direction that catches people: a capability defaulting to true has to be turned
off. A tv gets has_volume = true for free, so a projector that does not do volume must
say has_volume = false or it advertises four commands it cannot answer.
[[control]]
Every way into the hardware, in one table: the sockets core opens for you and the ports an installer patches you to. See Connections.
# A panel reached two ways. Which one a device uses is chosen while it is being added.
[[control]]
id = 1
kind = "tcp"
name = "Security protocol"
port = 12345
keepalive = true
[[control]]
id = 2
kind = "mqtt"
name = "IQ Remote protocol"
port = 8883
tls = true
# A projector on the end of a serial lead somebody ran.
[[control]]
id = 3
kind = "serial"
name = "Projector RS-232"
required = true
| Field | Required | Meaning |
|---|---|---|
id | required | Yours, stable across versions. A project remembers what it was wired to by this number, so renumbering one repoints a cable. |
kind | required | See the two kinds of connection below |
name | required | What the installer sees. Name the function, not the wire: "Open", not "Relay 1". A driver with two connections asks somebody which, and 1 and 2 is not a question anyone can answer. |
required | default true | Patched kinds only: false means the driver runs without it |
proxy | Which of your proxy bindings it belongs to |
Patched, or dialled
A connection is one or the other, and the kind decides which. Declaring a field that means
nothing for the kind fails junodrv check rather than being quietly ignored — a port on an
IR emitter is an author who believes something dials it.
| Kind | Extra fields | |
|---|---|---|
relay contact ir_out serial | Patched. The installer wires it to a port on another device — an iTach in the rack, the controller's own jack. Each names a proxy that provider must implement. | required, proxy |
network tcp mqtt hap | Dialled. Core opens it, at the address the device carries. | port, tls, keepalive, binary, username, password, probe |
zigbee | Joined. The node is joined to a mesh and adopted under a coordinator; the frames arrive decoded. See Discovery. | the zigbee/ table your package carries |
You may declare as many as the hardware has. Two of the same dialled kind is refused — nothing downstream could say which socket a device is on — but two different ones is the point: a panel that answers a security protocol on one port and an automation protocol on another is one product, and which of them a particular panel offers is how somebody configured it rather than what the hardware is.
A driver with more than one is a question when a device is added. The answer is recorded on the
device, decides which socket the controller opens, and is passed into your setup flow as
connection in its state — so a flow can branch on it, which is the point: one way in may want
a token typed off a settings page while the other runs a certificate exchange.
port is for the drivers whose socket core owns — the ones that send net_send or
publish. Leave it out if your driver builds its own URLs with http_request: nothing reads it
there, and a second copy of the port beside the one in your code is a copy that will disagree
with it. Leave it out too when the port is announced rather than fixed — a HAP accessory and an
Apple TV Companion link each pick one at boot and put it in their SRV record, which reaches you
as the device's own Port property and wins over this anyway.
Declaring a dialled connection at all is itself a statement: it says this driver holds a conversation with the hardware rather than shouting at it down an emitter, and core uses that to decide whether a television needs its screen's CEC line.
keepalive is for a device that pushes without being asked — an alarm panel, a telnet-speaking
amplifier. Core holds the connection, and when it drops, redials it and re-subscribes whatever
the driver had asked for. Without it a dropped socket stays dropped until the driver happens to
write again, which for a device that only pushes is for ever.
tls wraps the connection, and presents a client certificate if the device has one set — or
inherits one from its bridge. Without a certificate where one is required, the connection is
refused rather than downgraded.
kind decides patched-or-dialled and nothing finer: among the dialled ones, what actually
happens is decided by the fields around it and by which host calls the driver makes. net_send
gets a socket; publish/subscribe makes core an MQTT client on the device's behalf — one
connection, held open, reconnected and re-subscribed on its own. There is no broker in core and
there does not need to be: every MQTT device is one or has one beside it.
[control.probe]
How to recognize this hardware on a network it does not announce itself on. See Discovery.
[[control]]
id = 1
kind = "network"
name = "Telnet"
port = 23
[control.probe]
send = "PWSTANDBY\r"
expect = "PW"
Opt-in, and only ever run for drivers a controller already has installed.
[[connection]]
Signal connections — the AV graph. See Connections.
[[connection]]
id = 1001
proxy = 1
dir = "consumer"
class = "HDMI"
name = "HDMI 1"
[[connection]]
id = 4001
proxy = 1
dir = "provider"
class = "STEREO"
name = "Zone 2"
| Field | Required | Meaning |
|---|---|---|
id | required | Yours. Passed to set_input, reported by input_changed. |
proxy | required | Which of your proxy bindings owns this jack |
dir | required | consumer (input) or provider (output) |
class | required | Signal class |
name | required | The label silkscreened on the back panel |
Declare these only for hardware whose jacks you know without asking — a receiver whose input
list is fixed by its protocol, an emitter, a matrix of a stated size. A manifest describes a
product line, and a television's line does not have one back panel: four HDMI declarations give
a three-port set a jack the pathfinder will happily route a room through, and hide the fifth on
a set that has one. If your driver can ask the device, declare none and answer
HostCall::Connections on every bind instead — that replaces anything here for that one unit,
and is where every TV driver in the catalog gets its inputs from.
[[property]]
Installer-editable settings, shown in the device pane and readable from your driver with
host.get_property(name).
[[property]]
name = "PSK"
type = "password"
tooltip = "Pre-shared key from the TV's IP Control menu"
required = true
[[property]]
name = "Poll rate"
type = "ranged_int"
min = 1
max = 300
default = 10
unit = "s"
[[property]]
name = "Zone"
type = "list"
values = ["Main", "Zone 2", "Zone 3"]
default = "Main"
Types: string · password · int · ranged_int · float · bool · list ·
device_selector · color · label.
password values are stored encrypted and are never returned by the API, logged, or included
in a project export.
required means the device cannot work until somebody sets it — a Sonos API key, an account
name a driver cannot invent. It is distinct from merely having no default: plenty of properties
are optional and blank, and a screen that flagged all of them would flag nothing. It is
advisory, and your driver still checks. A driver that assumes core blocked an empty value
is one that panics the day somebody edits the project file by hand.
Your driver is called back on on_property_changed(name) when one is edited. Do not cache a
property across that callback.
A property that belongs to one connection
[[property]]
name = "Token"
type = "password"
required = true
connection = 1
For a driver reached more than one way, where the two want different answers. Untagged is the default and means it applies whichever way in the device uses, which is true of an address and of nearly everything else.
It is not cosmetic: a panel set up on its security protocol is not unfinished for want of a
client certificate it will never present, and flagging it hangs a warning off the tree that
nobody can clear. connection must name one this driver declares.
[[action]]
Something your driver can do that no proxy contract describes.
[[action]]
name = "permit_join"
label = "Allow devices to join"
description = "Opens the Zigbee network for sixty seconds."
confirm = true
[[action.arg]]
name = "seconds"
type = "int"
label = "For how long"
min = 10
max = 254
default = 60
A proxy command is a promise about a class of device: every light answers set_level, so
an automation, the assistant and the UI can all speak to one without knowing what it is. That
is the whole value of the layer, and it is why a driver cannot add to it.
But a Zigbee coordinator has to be told to open its network, and a Z-Wave controller has to be
told to heal its mesh. Those are not commands on a bridge — they are commands on this
bridge, and inventing bridge.permit_join would put a Zigbee concept in a contract a Hue
bridge also has to satisfy.
| Field | Default | Meaning |
|---|---|---|
name | required | Unique within the driver |
label | required | What the button says. Your language, not ours. |
description | A sentence under it | |
confirm | false | Ask first. For anything that removes a device or takes the mesh down. |
danger | false | A red button and a sentence explaining what is about to happen |
on | own | Which devices it belongs to: own, node, or a proxy type like sensor |
needs_one_of | Settings a device must have — any one — for the action to mean anything |
on narrows by kind; needs_one_of narrows within a kind. An SNZB-06P and a door contact are
both sensor and only one has a presence hold; that difference is in no contract, so the
driver reports it per device and this names what to look for.
[children]
What your driver may turn out to have behind it. Only for hubs: a bridge, a coordinator, an alarm panel — anything holding one connection on behalf of devices nobody could name when the manifest was written.
[children]
proxies = ["sensor", "security_partition"]
[[proxy]] is what your driver is; this is what it may find. The two are different questions
and only the first can be answered up front — a panel has as many zones as somebody programmed
into it, and declaring 128 sensor proxies so the largest possible panel fits gives every real
house a hundred empty bindings.
With this block you can report an inventory at runtime and core turns each entry into something an installer can adopt. See Runtimes.
It is a whitelist, not a switch. A driver that can present anything can present a lock,
and then a firmware quirk or a hostile answer from something on the network is a front door in
a project that no installer put there. Core drops a kind that is not on the list and logs why;
a name that is not a real contract fails at install, while you are still looking at the
manifest.
A driver with no [children] presents nothing. That is the right default for almost
everything — a television has no children, and one that grew some is a bug worth hearing about.
[adapter]
Present only when runtime = "adapter". The program to run, and what to run it with.
[adapter]
exec = "node"
args = ["zigbee/main.js"]
exec is either something on the controller's path or a binary shipped in the package. A
manifest saying adapter without this table describes nothing that can start. See
Runtimes.
[discovery]
How a controller recognizes your device on the network. Every matcher is optional; any hit offers the driver as a one-click bind. Full detail in Discovery.
[discovery]
mdns = ["_bravia._tcp", "_airplay._tcp"]
ssdp = ["urn:schemas-sony-com:service:ScalarWebAPI:1"]
sddp = ["sony:display"]
mac_oui = ["FC:F1:52", "54:42:49"]
An SDDP entry is either a bare type string or a table matching across several fields at once:
[[discovery.sddp]]
manufacturer = "Sony"
driver = "sony_display_XR*"
These hints are carried in the registry index as well as in the package, so a controller can match a device it just found against drivers that are not installed yet.
An unmatched device still shows up in the Discovery list — that list is how integrators find out a driver does not exist yet.