Juno Driver SDK
Everything needed to write a driver for Juno. This is the open part of Juno — the contract between a controller and the drivers it runs. Core itself is closed; nothing here depends on that, and writing a driver needs this documentation and the driver-sdk repository and nothing else.
| Guide | What it covers |
|---|---|
| Quickstart | A working driver, installed, in about ten minutes. Start here. |
| Connections | How a driver reaches its device — relays, contacts, IR, serial, network — and how AV signal is routed. |
| Runtimes | Declarative, native and adapter drivers, and the host calls they share. |
| Discovery | How a controller recognizes your hardware on the network. |
| Setup | Pairing, codes and link buttons — and what not to ask an installer for. |
| Publishing | Building a .junodrv, getting it certified, and shipping it to the registry. |
| Manifest reference | Every table and field in manifest.toml. |
| Proxy reference | Every device class, its commands, notifications and capabilities. Generated from the contracts a controller enforces, so it cannot drift. |
The one idea
Juno defines a device class once — a light is a light whether it is DALI, Zigbee, or a
relay behind a contactor. That definition is a proxy: a fixed set of commands,
notifications, and capability flags.
Your driver does two things:
- Declares which capabilities it has. Core computes the callable command set from that. A
light that cannot dim simply has no
set_level— not aset_levelthat fails. - Translates. Proxy command in, device bytes out. Device bytes in, proxy notification out.
Everything else — the UI, automations, AV routing, and the voice assistant — talks to the proxy and never to you. That is the point. It is also why you get all of them for free.
A driver in twenty lines
A gas fireplace. It has no protocol at all: it turns on when you close a contact. This is a real, complete, shippable driver.
manifest.toml
[driver]
id = "generic.fireplace.relay"
name = "Fireplace (relay)"
manufacturer = "Generic"
version = "1.0.0"
runtime = "declarative"
api = 1
[[proxy]]
id = 1
type = "switch"
capabilities = { has_auto_off = false }
# We need one relay. We do not care whose.
[[control]]
id = 1
kind = "relay"
name = "Fireplace valve"
required = true
commands.toml
command.on = { control = 1, invoke = "close" }
command.off = { control = 1, invoke = "open" }
command.toggle = { control = 1, invoke = "toggle" }
# Mirror the relay's own state back out as ours.
[[follow]]
control = 1
on = "relay_changed"
notify = "switch_changed"
args = { on = "$closed" }
Zip those two files as fireplace.junodrv and install it.
The installer binds control connection 1 to any relay in the project — a rack-mount relay board, a controller's onboard bank, an ESP32 someone built in a weekend. Your driver never learns which, and you never write that code. That is connections, and it is the part of this SDK worth understanding properly.
Choosing a runtime
Take the first one that works:
| Runtime | Use when | Cost |
|---|---|---|
| declarative | The device speaks fixed strings or codes: IR, RS-232, relay, contact | No code. A TOML file. |
| native | Real parsing, real state, a real protocol | A Rust crate and a compile step |
| adapter | A whole protocol stack already exists as a program — Zigbee, Z-Wave | Its own process, and its lifecycle to get right |
Most AV rack gear is declarative and stays that way. python and wasm appear in the manifest
format but no controller runs them yet — Runtimes says which is which, so you
do not find out at install time.
Rules that are not negotiable
- Declare capabilities honestly. A capability you declare and cannot deliver becomes a command the voice assistant offers a resident and then fails. Under-declaring is invisible; over-declaring is a support call.
- Notify on every state change, including ones you caused. Core's state store, the UI, and every automation trigger read from your notifications. A command that changes the device silently is a device that appears stuck.
- Never poll faster than you need. A house has hundreds of bindings on one controller.
- Do not assume your transport. If you hardcode a serial port you have broken every installation that puts the device behind an IP-to-serial bridge.
Where things live
| Where | What |
|---|---|
| junohouse/driver-sdk | The contract: proxy definitions, manifest and package formats, and junodrv. |
| junohouse/docs | This site. content/ is plain markdown; the site is a view over it. |
| driver.juno.house | The registry — every certified driver and the builds a controller installs from. |
| juno.house | The product. |