Skip to main content

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
FieldRequiredMeaning
idrequiredPermanent. Lowercase letters, digits, . and _. Reverse-dotted by convention, and the registry enforces that for anything certified.
namerequiredWhat a person sees
manufacturerGroups the catalog and the device list
versionrequiredSemver. How you ship an update.
runtimerequiredSee below
apidefault 1Host ABI version this driver was written against
min_coreLowest core version that can run it, as a semver requirement
primarydefault falseIn a multi-driver package, the one that names the artifact
parentDriver id of the bridge these devices live behind
productWhat a catalog calls the whole package, when that is not this driver's name
kindWhat the product is, as a proxy name. Groups the catalog and picks its icon.
variant_ofDriver id of the sibling this is another way of reaching
log_levelHow 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​

runtimeWhat ships in the package
declarativeA commands.toml, and nothing elseRuns
wasmOne driver.wasm, for every platform. Sandboxed.Runs
nativeA compiled library per platform. Not sandboxed — it runs in-process with the controller's privileges.Runs
adapterA whole protocol stack in its own process. Needs an [adapter] table.Runs
pythonA driver.pyAccepted by this format, refused by the controller
builtinNothing — core registers the driver itselfA 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:

  • product falls back to the lead driver's name, and the controller then trims the words a package's drivers all agree on: seven manifests beginning Philips Hue … produce Philips Hue without anybody writing it down. Declare it when they disagree — TP-Link's package is a TP-Link Account and a Tapo Dimmer Switch, which agree on nothing.
  • kind falls 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
FieldRequiredMeaning
idrequiredYours, stable across versions. A project remembers what it was wired to by this number, so renumbering one repoints a cable.
kindrequiredSee the two kinds of connection below
namerequiredWhat 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.
requireddefault truePatched kinds only: false means the driver runs without it
proxyWhich 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.

KindExtra fields
relay contact ir_out serialPatched. 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 hapDialled. Core opens it, at the address the device carries.port, tls, keepalive, binary, username, password, probe
zigbeeJoined. 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"
FieldRequiredMeaning
idrequiredYours. Passed to set_input, reported by input_changed.
proxyrequiredWhich of your proxy bindings owns this jack
dirrequiredconsumer (input) or provider (output)
classrequiredSignal class
namerequiredThe 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.

FieldDefaultMeaning
namerequiredUnique within the driver
labelrequiredWhat the button says. Your language, not ours.
descriptionA sentence under it
confirmfalseAsk first. For anything that removes a device or takes the mesh down.
dangerfalseA red button and a sentence explaining what is about to happen
onownWhich devices it belongs to: own, node, or a proxy type like sensor
needs_one_ofSettings 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.