# 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:

```json
{"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:

```json
{"BatteryLevel":{},"ChargingState":{}}
```

#### `presence-sensor`

Battery service:

```json
{"BatteryLevel":{},"ChargingState":{}}
```

Motion service:

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

Occupancy service:

```json
{"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:

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

even if they happen to work as a side effect.

Use empty objects:

```json
{"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:

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

- explicit documentation of required `characteristicProperties`
