Skip to main content

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.

GuideWhat it covers
ConnectionsHow a driver gets to its device: relays, contacts, IR, serial, network. Read this second.
RuntimesDeclarative, Python, and WASM drivers, and the host ABI they share.
Manifest Referencemanifest.toml — the file that describes your driver.
Proxy ReferenceEvery 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:

  1. Declares which capabilities it has. Core computes the callable command set from that. A light that cannot dim simply has no set_level — not a set_level that fails.
  2. 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 whenCost
declarativeThe device speaks fixed strings or codes: IR, RS-232, relay, contact, simple HTTPNo code. A TOML file.
pythonThere is a vendor SDK you would rather not reimplementOut of process; fine for anything that is not per-frame
wasmHot paths, real parsing, real state machinesCompile 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.