Juno Driver SDK
Everything needed to write a driver for Juno. This directory is the open part of Juno — the contract between core and third-party drivers. Core itself is closed; nothing in here depends on that.
| Guide | What it covers |
|---|---|
| Connections | How a driver gets to its device: relays, contacts, IR, serial, network. Read this second. |
| Runtimes | Declarative, Python, and WASM drivers, and the host ABI they share. |
| Manifest Reference | manifest.toml — the file that describes your driver. |
| Proxy Reference | Every device class, its commands, notifications, and capabilities. Generated from core's contracts, 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:
| Use when | Cost | |
|---|---|---|
| declarative | The device speaks fixed strings or codes: IR, RS-232, relay, contact, simple HTTP | No code. A TOML file. |
| python | There is a vendor SDK you would rather not reimplement | Out of process; fine for anything that is not per-frame |
| wasm | Hot paths, real parsing, real state machines | Compile step; sandboxed, hot-reloadable |
Most AV rack gear is declarative. Reach for WASM when you have actually found a reason.
Rules that are not negotiable
- Declare capabilities honestly. A capability you declare and cannot deliver becomes a command the voice assistant offers a user 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.