ADR 0007: HAL contract v1 — MQTT + HTTP, versioned JSON, HA discovery from the HAL
Status: Accepted Date: 2026-10-30
Context
Core and hald are separate processes, possibly on separate machines
(ADR-0004). The interface between them must be small, stable, discoverable, and
usable by Home Assistant with no core present.
Decision
Prefix seedypod/<unit-id>/. Payloads JSON, SI units, RFC 3339 timestamps.
unit-id is derived from the Pi serial (never a shared default).
| Topic | Dir | Retained | Payload |
|---|---|---|---|
availability | hald→ | yes | online / offline (LWT) |
system | hald→ | yes | SystemInfo — model, version, caps, limits |
sensor/<id> | hald→ | no | Reading {ts, value, unit, quality} |
sensor/<id>/calibrate | →hald | — | CalibrationCommand {point, value?} (added 2026-10-31; EZO probes) |
sensor/<id>/calibration | hald→ | yes | CalibrationResult {ts, point, ok, points, detail} |
devices | hald→ | yes | DeviceInventory {sensors, addable} — configured devices with status, and SensorKind descriptors (label, ids, bus, default address, calibration steps) for what can be added (added 2026-10-31) |
devices/set | →hald | — | DeviceCommand {requestId, op: add∣remove, kind, addr}; only app-added sensors can be removed |
devices/result | hald→ | no | DeviceResult {requestId, ok, detail} |
actuator/<id>/state | hald→ | yes | ActuatorState {on, level, since, fault, remainingS} |
actuator/<id>/set | →hald | — | ActuatorCommand {on?, level?, durationS?}; hald clamps or refuses |
event/button | hald→ | no | {press: single∣double∣long} |
alert/<kind> | hald→ | yes | Alert {kind, active, ts, detail} |
alert/<kind>/ack | →hald | — | {} clears a latched fault |
HTTP GET /health, /system, /camera/{upper∣lower}.jpg |
Home Assistant discovery (homeassistant/<component>/<unit-id>_<id>/config,
retained) is published by hald itself: light, pump as fan with percentage,
sensors with device_class, binary_sensor water_low, event for the button.
The typed contract lives twice — hal/crates/hal-core/src/contract.rs and
src/hardware/contract.ts — and hal/contract/fixtures/*.json are golden
samples both test suites must parse. The version is a field in system.
Consequences
- Core’s
MqttHalClient(Phase 3.1) andSimHalimplement the sameHalClientinterface; nothing above it knows which is live. - Adding a sensor (pH, EC, water temperature) is a new
SensorId, a fixture, and a driver — no topic changes.
Authors: @ng
Sources: sleepypod/core (conventions and ADR precedents) · iot-root/garden-of-eden (reference implementation, GPL-3; no code copied)
Imported from docs/adr/0007-hal-contract.md in seedypod/core. Edit it there, not here.