mynodes / .doc / homekit-adapter-development-guide.md
Newer Older
358 lines | 9.551kb
Bogdan Timofte authored 2 weeks ago
1
# HomeKit Adapter Development Guide
2

            
3
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`.
4

            
5
## Scope
6

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

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

            
13
Current adapters in scope:
14

            
15
- `adapters/water-leak-sensor/homekit-adapter`
16
- `adapters/presence-sensor/homekit-adapter`
17

            
18
## Core model
19

            
20
The adapter is responsible for:
21

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

            
28
`node-red-contrib-homekit-bridged` is responsible for:
29

            
30
- creating the HomeKit bridge and services
31
- exposing HAP characteristics
32
- publishing the bridge/accessories to HomeKit
33

            
34
Important practical detail:
35

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

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

            
41
## Established conventions
42

            
43
### 1. Bootstrap window
44

            
45
Adapters must use a bounded bootstrap window.
46

            
47
Current convention:
48

            
49
- `bootstrapDeadlineMs = 10000`
50

            
51
Reason:
52

            
53
- retained messages may be missing
54
- waiting forever leaves HomeKit on defaults forever
55
- `10s` is long enough for flow initialization and bridge publication, while still reasonable for startup
56

            
57
### 2. Hold-until-timeout bootstrap
58

            
59
Current established behavior:
60

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

            
67
Reason:
68

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

            
73
### 3. Snapshot over delta at bootstrap
74

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

            
77
Do not rely on:
78

            
79
- incremental updates only
80
- the first retained message being enough
81
- HomeKit reconstructing full state from partial early updates
82

            
83
### 4. Unknown values are not invented
84

            
85
At bootstrap timeout:
86

            
87
- emit all known values
88
- leave unknown services/characteristics as `null` output for that cycle
89

            
90
Do not fabricate values just to fill a payload.
91

            
92
### 5. Cache bypass for bootstrap snapshot
93

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

            
96
Reason:
97

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

            
101
## Topic and subscription conventions
102

            
103
### `water-leak-sensor`
104

            
105
Current supported model:
106

            
107
- `SNZB-05P`
108

            
109
Subscribe on startup to:
110

            
111
- `.../last`
112
- `.../value`
113
- `.../availability`
114

            
115
Bootstrap is based on:
116

            
117
- `water_leak`
118
- `battery` or `battery_low`
119

            
120
### `presence-sensor`
121

            
122
Current supported model:
123

            
124
- `ZG-204ZV`
125

            
126
Subscribe on startup to:
127

            
128
- `.../last`
129
- `.../value`
130

            
131
Bootstrap is based on:
132

            
133
- `motion`
134
- `temperature`
135
- `humidity`
136
- `illuminance`
137
- `battery` or `battery_low`
138

            
139
### Unsubscribe rule
140

            
141
Keep `.../last` subscribed during bootstrap.
142

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

            
145
## Payload conventions
146

            
147
### General
148

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

            
151
Use explicit conversions:
152

            
153
- booleans via tolerant parsing
154
- numbers via tolerant parsing
155
- clamp to HAP-valid ranges
156

            
157
### Battery payload
158

            
159
Current convention:
160

            
161
```json
162
{"StatusLowBattery":0,"BatteryLevel":100,"ChargingState":2}
163
```
164

            
165
Notes:
166

            
167
- `BatteryLevel` is `uint8`, integer `0..100`
168
- `ChargingState = 2` means `NOT_CHARGEABLE`
169
- do not send floats for battery level
170

            
171
### Motion/occupancy status flags
172

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

            
175
- `StatusActive`
176
- `StatusFault`
177
- `StatusLowBattery`
178
- `StatusTampered`
179

            
180
These should be derived from local state, not omitted.
181

            
182
## Optional characteristics and `homekit-service` configuration
183

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

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

            
189
- ignore them during startup
190
- keep default values
191
- only reflect them after a later update
192

            
193
### Required `characteristicProperties`
194

            
195
#### `water-leak-sensor`
196

            
197
Battery service:
198

            
199
```json
200
{"BatteryLevel":{},"ChargingState":{}}
201
```
202

            
203
#### `presence-sensor`
204

            
205
Battery service:
206

            
207
```json
208
{"BatteryLevel":{},"ChargingState":{}}
209
```
210

            
211
Motion service:
212

            
213
```json
214
{"StatusActive":{},"StatusFault":{},"StatusLowBattery":{},"StatusTampered":{}}
215
```
216

            
217
Occupancy service:
218

            
219
```json
220
{"StatusActive":{},"StatusFault":{},"StatusLowBattery":{},"StatusTampered":{}}
221
```
222

            
223
### Why this is needed
224

            
225
Observed behavior during development:
226

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

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

            
233
### What not to do
234

            
235
Do not use boolean values like:
236

            
237
```json
238
{"BatteryLevel":true,"ChargingState":true}
239
```
240

            
241
even if they happen to work as a side effect.
242

            
243
Use empty objects:
244

            
245
```json
246
{"BatteryLevel":{},"ChargingState":{}}
247
```
248

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

            
251
## Delay and timing rules
252

            
253
### Adapter startup delay
254

            
255
Current adapters use:
256

            
257
- `startSubscriptions` after `250ms`
258

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

            
261
### Bridge publication delay
262

            
263
Observed in `node-red-contrib-homekit-bridged`:
264

            
265
- bridge publication is delayed by `5000ms`
266

            
267
Implication:
268

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

            
271
### Practical readiness window
272

            
273
Observed in practice:
274

            
275
- effective readiness was around `7-10s`
276

            
277
Current convention therefore remains:
278

            
279
- bootstrap deadline at `10s`
280
- emit final bootstrap snapshot at that deadline
281

            
282
## Design rules for new adapters
283

            
284
When writing a new HomeKit adapter:
285

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

            
297
## Testing workflow
298

            
299
Recommended order:
300

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

            
312
## Deployment notes
313

            
314
Known hosts:
315

            
316
- `testing`: `node-red@192.168.2.104`
317
- `production`: `node-red@192.168.2.101`
318
- `legacy`: `pi@192.168.2.133`
319

            
320
Important service name difference observed:
321

            
322
- on `testing`, restart `node-red.service`
323
- do not assume the unit is named `nodered.service`
324

            
325
## Documentation rules for adapters
326

            
327
Each HomeKit adapter help page should document:
328

            
329
- startup subscription behavior
330
- bootstrap rule
331
- output payload shapes
332
- required optional characteristics on linked `homekit-service` nodes
333
- exact `characteristicProperties` JSON as copy-pasteable code blocks
334

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

            
337
## Known pitfalls
338

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

            
345
## Current repository defaults
346

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

            
349
- startup subscription delay: `250ms`
350
- bootstrap deadline: `10000ms`
351
- final bootstrap strategy: hold-until-timeout, then emit one full snapshot
352
- battery payload:
353

            
354
```json
355
{"StatusLowBattery":0,"BatteryLevel":100,"ChargingState":2}
356
```
357

            
358
- explicit documentation of required `characteristicProperties`