A minimal HomeKit bridge for Zigbee2MQTT. Exposes Z2M lights, smart plugs, and scenes to Apple Home for HomePod and Siri voice commands.
It also provides an HTTP endpoint with a live-updating dashboard presenting all data in both MQTT and HAP forms, as well as Prometheus metrics and health check endpoints.
Note that Hoboken is a HAP bridge, not a client. It provides bidirectional control of MQTT devices and scenes to HomeKit. It does not provide control of HomeKit-only devices (e.g., Ecobee thermostats) from MQTT. That would be a different project.
Matterbridge and Homebridge are too complex and hard to troubleshoot. Hoboken does one
thing: bridge Z2M to HomeKit. It's built directly on
hap-nodejs.
It does not have any device discovery, self-update, or plugins. You get exactly what you configure, and you will always get that.
All dependencies are pure JS/TS code. Test coverage is enforced at 100% line, function, and statement coverage.
The deployment is a reproducible, distroless container image. You can run it on K3s or whatever.
It's called "Hoboken" because that has some of the same letters as "HomeKit Bridge".
| Module | Responsibility |
|---|---|
src/config.ts |
Load and validate YAML config |
src/convert.ts |
Value conversion (Z2M ↔ HomeKit) |
src/accessories.ts |
Create HAP accessories and scenes |
src/bridge.ts |
MQTT client, bridge wiring, lifecycle |
src/metrics.ts |
Prometheus metrics and HTTP server |
src/main.ts |
Entry point |
Modules are split for testability. Each has clear inputs/outputs and minimal
coupling. Accessory creation takes injected publish/getState functions
rather than an MQTT client directly.
Devices declare capabilities in config.yaml:
| Capability | HomeKit Characteristic | Conversion |
|---|---|---|
on_off |
On |
boolean, always present |
brightness |
Brightness |
Z2M 0–254 ↔ HomeKit 0–100 |
color_temp |
ColorTemperature |
mireds, no conversion needed |
color_hs |
Hue + Saturation |
ranges match, no conversion |
Scenes are exposed as momentary switches. "Hey Siri, turn on Movie Mode"
publishes {"scene_recall": <id>} to Z2M. The switch auto-resets to off after
1 second.
- MQTT disconnect:
onSetthrowsHapStatusError(SERVICE_COMMUNICATION_FAILURE)→ HomeKit shows "No Response" - MQTT reconnect: re-subscribes and refreshes state automatically
- Config changes: restart the container (no hot reload)
Opt-in Prometheus metrics via prom-client. Add a metrics section to
config.yaml with a port to enable.
| Metric | Type | Labels |
|---|---|---|
hoboken_mqtt_connected |
Gauge | |
hoboken_mqtt_messages_received_total |
Counter | device |
hoboken_mqtt_messages_published_total |
Counter | |
hoboken_mqtt_errors_total |
Counter | |
hoboken_devices_configured |
Gauge |
Default Node.js process metrics (CPU, memory, event loop lag) are also included.
The k8s/deployment.yaml includes Prometheus scrape annotations.
When metrics are enabled, GET / serves an HTML status page showing:
- Bridge name and version
- MQTT connection status and active HAP connection count
- Each device with its topic, capabilities, scenes, and current state
This is useful for quick diagnostics without checking logs or Prometheus. The page loads new data in real time using SSE.
To preview the status page with sample data:
bun run demo:statusbridge:
name: "Hoboken"
mac: "AA:BB:CC:DD:EE:FF" # generate a unique MAC for your bridge
pincode: "123-45-678" # change this before pairing
port: 51826
bind: "eno1" # optional: network interface for mDNS (needed in k8s with hostNetwork)
mqtt:
url: "mqtt://mosquitto:1883"
topic_prefix: "zigbee2mqtt"
# metrics:
# port: 9090
# bind: "127.0.0.1" # optional: defaults to 0.0.0.0
devices:
- name: "Living Room Light"
topic: "living_room_light"
capabilities: [on_off, brightness, color_temp]
scenes:
- name: "Movie Mode"
id: 1
- name: "Bedroom Light"
topic: "bedroom_light"
capabilities: [on_off, brightness, color_temp, color_hs]Neither username nor pincode are secrets. The MAC is broadcast via mDNS.
The PIN is only used during initial pairing and is not reusable afterward.
The optional bind field restricts the HAP server and mDNS advertisement to a
specific network interface. Without it, hap-nodejs advertises on all interfaces,
which in Kubernetes with hostNetwork: true can cause mDNS to advertise a
cluster-internal IP (e.g., cni0) instead of the LAN IP. Set bind to the
host's LAN interface name (e.g., eno1) so HomeKit clients can reach the bridge.
The bridge MAC must be a locally-administered unicast address (bit 1 of the
first octet set, bit 0 clear). The PIN is 8 random digits formatted as
XXX-XX-XXX and must not be a HAP-excluded value (e.g., 000-00-000,
111-11-111, 123-45-678).
Generate both with:
python3 -c "import random; b=random.randbytes(6); print(f'MAC: {b[0]|2&~1:02X}:{b[1]:02X}:{b[2]:02X}:{b[3]:02X}:{b[4]:02X}:{b[5]:02X}'); d=[random.randint(0,9) for _ in range(8)]; print(f'PIN: {d[0]}{d[1]}{d[2]}-{d[3]}{d[4]}-{d[5]}{d[6]}{d[7]}')"Hoboken can target Z2M groups by using the group's friendly name as the device topic. Commands are broadcast to all group members at the Zigbee level. Devices that don't support a given cluster silently ignore it, so capabilities should match the most capable member. This is preferable to homebridge-z2m's approach of intersecting capabilities.
- Bun (package manager and test runner)
- Node.js 24+ (runtime, for TypeScript type stripping)
bun installbun run check # TypeScript type checking
bun run lint # ESLint (strict + stylistic)
bun test # Run tests
bun test --coverage # Tests with coverage report
bun run start # Run with Node.jsdocker build -t hoboken .Multi-stage build: Bun installs deps, distroless Node.js 24 runs the app.
Image: gcr.io/distroless/nodejs24-debian13 (no shell, minimal attack surface).
Deployed on k3s via Flux. The manifests in k8s/ are
reconciled automatically.
hostNetwork: true— required for mDNS (HomeKit device discovery)strategy: Recreate— single instance (MAC uniqueness)- PVC at
/persist— pairing data survives restarts - ConfigMap at
/config/config.yaml
- Open the Home app on your iPhone or iPad.
- Tap + (top right) → Add Accessory.
- Tap More options… at the bottom. "Hoboken" should appear via mDNS.
- Select it, then enter the PIN from your config (e.g.,
482-37-159). - Assign the bridge to a room when prompted.
The PIN is only used during the initial pairing. After that, the pairing keys
stored in /persist authenticate all communication. If you delete the PVC or
change the MAC, you must remove the bridge from Home and re-pair.
Verify mDNS advertisement from a Mac on the same network:
dns-sd -B _hap._tcpCheck that the HAP port is reachable from the network:
nc -z <node-ip> 51826View logs for pairing diagnostics:
kubectl -n hoboken logs deploy/hobokenThe bridge logs lifecycle events: MQTT connection state, HAP server listening address, mDNS advertisement, identify requests, pair-setup completion, and connection close events.
