Skip to content

feat(z2m): Zigbee2MQTT driver (v0.1)#6

Merged
fdatoo merged 16 commits into
mainfrom
feat/zigbee2mqtt
May 1, 2026
Merged

feat(z2m): Zigbee2MQTT driver (v0.1)#6
fdatoo merged 16 commits into
mainfrom
feat/zigbee2mqtt

Conversation

@fdatoo
Copy link
Copy Markdown
Owner

@fdatoo fdatoo commented May 1, 2026

Summary

  • New driver drivers/z2m/ (Carport driver.z2m) — MQTT-based Zigbee2MQTT integration. Surfaces lights, numeric sensors, and binary sensors from a Z2M deployment with hot add/remove via the retained bridge/devices topic.
  • New driverkit package gohome-driverkit/colorconv/ — pure CIE-xy ↔ RGB / HSV ↔ RGB / gamut clamping. Hue migrated to consume it; Z2M uses it for color state merges.
  • Integration tested end-to-end against an in-process mochi-mqtt broker: initial reconciliation, command round-trip, hot add/remove, bridge-offline propagation.

Implements the plan at docs/design/plans/2026-04-30-z2m-driver.md.

Architecture

  • internal/z2m/ — pure topic constructors + JSON payload types (Device, Expose, BridgeStatePayload, StatePayload).
  • internal/state/ — pure state translation: EntityID, EntitiesFor, MergeState, Reconcile, CommandToPayload. No I/O.
  • internal/mqtt/ — thin paho.mqtt.golang wrapper with auto-reconnect and subscription re-assertion on connect.
  • cmd/z2m-driver/main.go wires everything via the driverkit; main_test.go runs end-to-end against an in-process mochi broker.

What gets surfaced (v0.1)

  • Lights → one light.* entity per device with turn_on, turn_off, set_brightness, set_color_temp, set_color.
  • Numeric sensors (temperature, humidity, illuminance, battery, pressure, power, energy, current) → numeric_sensor.*.
  • Binary sensors (occupancy, contact, water_leak, smoke, tamper, vibration) → binary_sensor.*.
  • Multi-property devices fan out — a motion sensor with occupancy + temperature + humidity + battery yields four entities.

Out of scope in v0.1: switches/smart-plug actuators (read-only sub-properties still surface), action sensors, climate/cover/lock/fan classes, Z2M network management.

Test plan

  • go test ./gohome-driverkit/... ./drivers/... — 49 tests, 0 failures
  • go test ./drivers/z2m/cmd/z2m-driver/... -race -count=2 -timeout 120s — clean
  • go run ./drivers/z2m/cmd/z2m-driver with no env → fails with Z2M_BROKER_URL is required, exit 1
  • Smoke test against a real Z2M deployment (deferred to follow-up; broker integration covered by mochi tests)

🤖 Generated with Claude Code

fdatoo and others added 16 commits May 1, 2026 00:07
New shared package gohome-driverkit/colorconv/ holds CIE-xy ↔ RGB,
HSV ↔ RGB, gamut clamping, and packed RGB helpers. Extracted from
drivers/hue/internal/bridge/colormath.go to be consumed by Hue and
Z2M drivers (and third-party drivers).

Hue still owns its own copy in this commit; Task 2 migrates it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hue's private color math moves to gohome-driverkit/colorconv (added
in the previous commit). bridge.ColorXY and bridge.Gamut become
type aliases for backwards compatibility on JSON wire types; the
math functions are dropped from the bridge package.

This unblocks the Z2M driver from sharing the same pure-math code
path without importing from another driver's internal/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the drivers/z2m/ directory tree (cmd, internal/mqtt,
internal/z2m, internal/state) with doc.go package headers.
Pulls in paho.mqtt.golang for production use and
mochi-mqtt/server/v2 for in-process broker testing.

No driver logic yet — subsequent commits fill in topics, payloads,
state translation, the MQTT wrapper, and main wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
drivers/z2m/internal/z2m/ models the Z2M topic namespace
(BridgeDevices, BridgeState, BridgeEvent, DeviceTopics) and the
JSON payload shapes (Device, Definition, Expose, BridgeState,
AvailabilityState, StatePayload). Decoding is verified against
a captured bridge/devices fixture covering a colour light,
multi-sensor, contact sensor, smart plug, and coordinator.

Pure types, no I/O. Used by the reconciler and main wiring in
later tasks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lights → light.z2m_<last8hex>; sensors → <kind>.z2m_<last8hex>_<prop>.
Short, stable across friendly_name changes, collision-free within
one Z2M instance.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks the device's exposes tree, collapsing 'light' composites
into one light.* entity and fanning per-property numeric/binary
leaves into numeric_sensor.* / binary_sensor.* entities. Applies
a blocklist for noisy properties (linkquality, voltage,
update_available, last_seen) and skips writable non-light
properties in v0.1 (Switch class out of scope) with INFO log.

Verified against the captured bridge/devices fixture: colour light
yields one light.* with four capabilities; the multi-sensor yields
one binary_sensor and three numeric_sensor entities; the contact
sensor yields one binary_sensor and one numeric_sensor; the smart
plug yields only its read-only power; coordinator yields zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Maps Carport capabilities (turn_on, turn_off, set_brightness,
set_color_temp, set_color) to the JSON payloads Z2M's /set topic
expects. Range validation runs before any network I/O so bad input
surfaces as CARPORT_INTERNAL synchronously.

set_color emits color: {hex: "#RRGGBB"} — Z2M understands hex
across every vendor and avoids per-bulb gamut clamping (Z2M does
its own).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-kind dispatch: Light merges state/brightness/color_temp/color
fields (with mutual exclusivity between color and color_temp);
NumericSensor sets value preserving unit; BinarySensor accepts
both bool and string ("ON"/"OFF") payloads, since Z2M devices
disagree on representation.

Unknown light properties are no-ops rather than errors — a
multi-property state push fans out to several entities and each
ignores keys it doesn't recognise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AddEntity / UnregisterEntity / UpdateAttrs cover the three
mutations the bridge/devices retained topic produces:
new pairings, removed devices, and friendly_name renames.
Adds come before removes in the ordered list so subscribe
happens before registration on a swap (avoids retained-state
race), with UpdateAttrs last.

Composition changes (device firmware grows a property) are
deferred — user restarts the driver to pick them up; documented
in README later.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Thin facade exposing Connect/Subscribe/Unsubscribe/Publish/Close
plus OnConnect/OnDisconnect callbacks. paho's auto-reconnect is
left on; the OnConnect callback re-asserts subscriptions so
reconnects don't silently drop them.

Tested against an in-process mochi-mqtt broker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reads Z2M_BROKER_URL / Z2M_USERNAME / Z2M_PASSWORD /
Z2M_BASE_TOPIC / Z2M_CLIENT_ID / Z2M_TLS_SKIP_VERIFY from env;
dials the broker via the internal/mqtt wrapper; constructs the
driver and a stateCache; forwards broker connect/disconnect to
driver events.

The reconciliation handlers (subscribeBridgeTopics) land next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires up the four bridge subscriptions (devices, state, event,
plus per-device state/availability) into the Reconcile output:
AddEntity registers entity + capability handlers + subscribes
state/availability; UnregisterEntity unsubscribes + drops the
entity; UpdateAttrs re-emits last attrs with the new label.

State-topic payloads fan out to every entity that listens on
the topic (a multi-property device hands the same payload to
several entities, each consuming its own property).

Integration-tested against an in-process mochi-mqtt broker:
initial reconciliation, turn_on round-trip via /set, hot
add/remove on bridge/devices republish, bridge-offline
propagation to per-entity Available=false.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds drivers/z2m/README.md with quick-start + caveats and
updates the existing zigbee2mqtt entry in
docs/docs/drivers/first-party.md to reflect the actual driver
(driver.z2m, env-var config, three device classes, known
limitations).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both drivers previously read driver-specific env vars (HUE_BRIDGE_ADDRESS,
Z2M_BROKER_URL, ...) directly. The supervisor only sets the four
GOHOME_CARPORT_* env vars when launching a driver, so those reads
worked only via accidental shell inheritance — without per-instance
scoping.

Switch both drivers to the canonical mechanism: parse the JSON blob
in GOHOME_CARPORT_INSTANCE_CONFIG into a typed config struct.
Secrets stay out of the config blob — they live in named env vars
referenced by *_env fields (password_env, api_key_env), matching the
existing convention in the first-party catalog page and the MQTT
driver entry.

Operational toggles (HUE_LOG_LEVEL, Z2M_LOG_LEVEL) remain env vars
since they're per-process, not per-instance.

READMEs and the first-party catalog page updated to document the
JSON shape with example TOML config_json blocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- go.mod / go.sum: paho.mqtt.golang and mochi-mqtt/server/v2 promoted
  to direct deps (they're imported by drivers/z2m); kr/text and
  creack/pty pruned, jinzhu/copier checksum added.
- drivers/z2m/cmd/z2m-driver/main.go: split main into run() returning
  an error so os.Exit no longer skips deferred cleanup (gocritic);
  reformat stateCache field alignment (goimports); drop ineffectual
  available=true initializer in onDeviceAvailability (ineffassign).
- drivers/z2m/internal/state/mapping.go: preallocate EntitiesFor's
  out slice (prealloc).
- drivers/z2m/internal/state/reconcile_test.go: drop redundant int
  type from var declaration (staticcheck QF1011).
- gohome-driverkit/colorconv/colorconv_test.go: gofmt cleanup of
  TestHSVToRGBKnownPoints case alignment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fdatoo fdatoo merged commit 0e22aa6 into main May 1, 2026
12 checks passed
@fdatoo fdatoo deleted the feat/zigbee2mqtt branch May 1, 2026 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant