mynodes / .doc / homekit-adapter-development-guide.md
1 contributor
358 lines | 9.551kb

HomeKit Adapter Development Guide

This guide documents the conventions and lessons learned while building and debugging the custom HomeKit adapters in this repository and their interaction with node-red-contrib-homekit-bridged.

Scope

Applies to the custom Node-RED adapters in this repository that:

  • consume retained/live telemetry from MQTT/HomeBus-like topics
  • translate that state into HomeKit payloads
  • feed homekit-service nodes from node-red-contrib-homekit-bridged

Current adapters in scope:

  • adapters/water-leak-sensor/homekit-adapter
  • adapters/presence-sensor/homekit-adapter

Core model

The adapter is responsible for:

  • subscribing to retained bootstrap data and live updates
  • keeping a local sensor state model
  • mapping that model to HomeKit service payloads
  • deciding when bootstrap is complete
  • emitting a full state snapshot once the HomeKit side is ready enough to consume it

node-red-contrib-homekit-bridged is responsible for:

  • creating the HomeKit bridge and services
  • exposing HAP characteristics
  • publishing the bridge/accessories to HomeKit

Important practical detail:

  • node-red-contrib-homekit-bridged delays bridge publication by 5s
  • in practice, usable HomeKit readiness was observed around 7-10s after startup

This means early messages can be accepted by Node-RED but still be ineffective for HomeKit initialization.

Established conventions

1. Bootstrap window

Adapters must use a bounded bootstrap window.

Current convention:

  • bootstrapDeadlineMs = 10000

Reason:

  • retained messages may be missing
  • waiting forever leaves HomeKit on defaults forever
  • 10s is long enough for flow initialization and bridge publication, while still reasonable for startup

2. Hold-until-timeout bootstrap

Current established behavior:

  • subscribe to .../last and live topics at startup
  • collect retained and live values into local state
  • do not immediately finalize bootstrap when all values arrive early
  • keep bootstrap open until the 10s bootstrap deadline
  • at deadline, emit one full snapshot and unsubscribe from .../last

Reason:

  • messages emitted before the bridge is fully usable may not initialize HomeKit correctly
  • without downstream feedback, repeated retries are arbitrary load
  • a single deterministic final bootstrap snapshot is easier to reason about

3. Snapshot over delta at bootstrap

At bootstrap end, always emit a full snapshot of the known service state.

Do not rely on:

  • incremental updates only
  • the first retained message being enough
  • HomeKit reconstructing full state from partial early updates

4. Unknown values are not invented

At bootstrap timeout:

  • emit all known values
  • leave unknown services/characteristics as null output for that cycle

Do not fabricate values just to fill a payload.

5. Cache bypass for bootstrap snapshot

Adapters may deduplicate repeated payloads during normal operation, but the final bootstrap snapshot must bypass local deduplication.

Reason:

  • the same values may have been seen during early startup
  • HomeKit still needs one authoritative post-bootstrap snapshot

Topic and subscription conventions

water-leak-sensor

Current supported model:

  • SNZB-05P

Subscribe on startup to:

  • .../last
  • .../value
  • .../availability

Bootstrap is based on:

  • water_leak
  • battery or battery_low

presence-sensor

Current supported model:

  • ZG-204ZV

Subscribe on startup to:

  • .../last
  • .../value

Bootstrap is based on:

  • motion
  • temperature
  • humidity
  • illuminance
  • battery or battery_low

Unsubscribe rule

Keep .../last subscribed during bootstrap.

Unsubscribe from .../last only after the final bootstrap snapshot was emitted.

Payload conventions

General

Adapters should maintain a local normalized state and derive HomeKit payloads from that state, not directly forward upstream messages.

Use explicit conversions:

  • booleans via tolerant parsing
  • numbers via tolerant parsing
  • clamp to HAP-valid ranges

Battery payload

Current convention:

{"StatusLowBattery":0,"BatteryLevel":100,"ChargingState":2}

Notes:

  • BatteryLevel is uint8, integer 0..100
  • ChargingState = 2 means NOT_CHARGEABLE
  • do not send floats for battery level

Motion/occupancy status flags

For services that include status flags, publish explicit values for:

  • StatusActive
  • StatusFault
  • StatusLowBattery
  • StatusTampered

These should be derived from local state, not omitted.

Optional characteristics and homekit-service configuration

This is the main practical integration rule with node-red-contrib-homekit-bridged.

Some HomeKit characteristics are optional in HAP, but the adapter still publishes them. If they are not materialized on the homekit-service node, HomeKit may:

  • ignore them during startup
  • keep default values
  • only reflect them after a later update

Required characteristicProperties

water-leak-sensor

Battery service:

{"BatteryLevel":{},"ChargingState":{}}

presence-sensor

Battery service:

{"BatteryLevel":{},"ChargingState":{}}

Motion service:

{"StatusActive":{},"StatusFault":{},"StatusLowBattery":{},"StatusTampered":{}}

Occupancy service:

{"StatusActive":{},"StatusFault":{},"StatusLowBattery":{},"StatusTampered":{}}

Why this is needed

Observed behavior during development:

  • optional characteristics can be ignored silently during startup
  • HomeKit may expose default values like BatteryLevel = 0 and ChargingState = Not Charging
  • the values become correct only after a later update, if at all

The characteristicProperties JSON above forces the relevant optional characteristics to be materialized on the corresponding homekit-service.

What not to do

Do not use boolean values like:

{"BatteryLevel":true,"ChargingState":true}

even if they happen to work as a side effect.

Use empty objects:

{"BatteryLevel":{},"ChargingState":{}}

This is clearer and aligns with the intended structure of characteristicProperties.

Delay and timing rules

Adapter startup delay

Current adapters use:

  • startSubscriptions after 250ms

This is acceptable for subscription startup, but it is not enough to assume HomeKit readiness.

Bridge publication delay

Observed in node-red-contrib-homekit-bridged:

  • bridge publication is delayed by 5000ms

Implication:

  • messages earlier than roughly 5s after startup should not be trusted as bootstrap-finalizing messages for HomeKit behavior

Practical readiness window

Observed in practice:

  • effective readiness was around 7-10s

Current convention therefore remains:

  • bootstrap deadline at 10s
  • emit final bootstrap snapshot at that deadline

Design rules for new adapters

When writing a new HomeKit adapter:

  1. Build a local state model first.
  2. Normalize all incoming data before mapping to HomeKit.
  3. Define which capabilities are mandatory for bootstrap completion.
  4. Subscribe to retained and live data separately if needed.
  5. Use a bounded bootstrap deadline.
  6. Emit one full bootstrap snapshot at the end of the bootstrap window.
  7. Unsubscribe from retained/bootstrap topics only after the final snapshot.
  8. Document every optional HomeKit characteristic that must be enabled on downstream homekit-service nodes.
  9. Keep payloads deterministic and range-safe.
  10. Prefer explicit service-specific builders like buildBatteryMsg, buildMotionMsg, etc.

Testing workflow

Recommended order:

  1. Validate local code changes.
  2. Deploy to testing first.
  3. Restart node-red.service on 192.168.2.104.
  4. Observe startup behavior for at least 10s.
  5. Verify:
    • bootstrap snapshot timing
    • optional characteristic visibility in HomeKit
    • retained topic unsubscribe behavior
    • no silent fallback to default characteristic values
  6. Only then promote to production.

Deployment notes

Known hosts:

  • testing: node-red@192.168.2.104
  • production: node-red@192.168.2.101
  • legacy: pi@192.168.2.133

Important service name difference observed:

  • on testing, restart node-red.service
  • do not assume the unit is named nodered.service

Documentation rules for adapters

Each HomeKit adapter help page should document:

  • startup subscription behavior
  • bootstrap rule
  • output payload shapes
  • required optional characteristics on linked homekit-service nodes
  • exact characteristicProperties JSON as copy-pasteable code blocks

This is not optional. The downstream Node-RED flow configuration is part of the adapter contract.

Known pitfalls

  • Early retained messages can arrive before HomeKit is effectively ready.
  • Optional HAP characteristics can be ignored silently at startup.
  • Default HAP values can look like valid values while actually meaning "not initialized yet".
  • JSON key order is not the root cause for characteristic loss.
  • waitForSetupMsg is for deferred node configuration, not for runtime bootstrap value materialization.

Current repository defaults

Unless a strong reason appears, keep these defaults for new HomeKit adapters:

  • startup subscription delay: 250ms
  • bootstrap deadline: 10000ms
  • final bootstrap strategy: hold-until-timeout, then emit one full snapshot
  • battery payload:
{"StatusLowBattery":0,"BatteryLevel":100,"ChargingState":2}
  • explicit documentation of required characteristicProperties