mqtt_bus / adapter_implementation_examples.md
Newer Older
322 lines | 9.045kb
Bogdan Timofte authored 2 weeks ago
1
# Adapter Implementation Examples
2

            
3
## Purpose
4

            
5
This document captures practical implementation conventions that proved useful while building the `ZG-204ZV` adapters:
6

            
7
- Zigbee2MQTT -> HomeBus adapter
8
- HomeBus -> HomeKit adapter
9

            
10
It complements `addapters.md`.
11

            
12
`addapters.md` defines the normative adapter role and contract boundaries.
13

            
14
This document focuses on practical Node-RED integration patterns, cold-start behavior, flow topology, and known failure modes.
15

            
16
---
17

            
18
## Two Distinct Adapter Roles
19

            
20
In practice, two different adapter classes appear in the system:
21

            
22
1. ingress adapters
23

            
24
These consume vendor topics and publish canonical bus topics.
25

            
26
Example:
27

            
28
`zigbee2mqtt/ZG-204ZV/...` -> `vad/home/...`
29

            
30
2. consumer adapters
31

            
32
These consume canonical bus topics and project them into another consumer model.
33

            
34
Example:
35

            
36
`vad/home/...` -> HomeKit
37

            
38
Rule:
39

            
40
- keep ingress normalization separate from consumer-specific projection
41
- do not mix HomeKit, dashboard, or automation constraints into the ingress adapter
42
- treat HomeKit adapters as bus consumers, not as protocol translators
43

            
44
Pipeline:
45

            
46
Device -> Ingress Adapter -> MQTT Bus -> Consumer Adapter -> Consumer System
47

            
48
---
49

            
50
## Recommended Node-RED Flow Topology
51

            
52
### Ingress Adapter Pattern
53

            
54
Recommended structure:
55

            
56
1. dynamic or static `mqtt in` on vendor broker
57
2. adapter normalization node
58
3. `mqtt out` on canonical broker
59
4. optional debug nodes on raw input and normalized output
60

            
61
If the adapter controls vendor subscription dynamically:
62

            
63
- reserve one output for `mqtt in` control messages
64
- reserve one output for MQTT-ready publish messages
65

            
66
Practical convention:
67

            
68
- when an adapter has multiple semantic outputs, keep the `mqtt in` control output as the last output
69
- this keeps semantic outputs stable and makes flow rewiring easier
70

            
71
### Consumer Adapter Pattern
72

            
73
Recommended structure:
74

            
75
1. dynamic `mqtt in` on canonical broker
76
2. consumer adapter
77
3. consumer-specific nodes or services
78
4. optional debug nodes for `last`, `value`, and control messages
79

            
80
Typical example:
81

            
82
- subscribe to `.../last` and `.../value`
83
- use retained `last` for cold start
84
- continue on live `value`
85
- optionally unsubscribe from `.../last` after bootstrap completes
86

            
87
---
88

            
89
## Cold-Start Pattern with `last`
90

            
91
For stateful consumers such as HomeKit, a practical pattern is:
92

            
93
1. subscribe to `.../last`
94
2. subscribe to `.../value`
95
3. initialize consumer state from retained `last`
96
4. keep consuming live `value`
97
5. optionally unsubscribe from `.../last` after bootstrap completes
98

            
99
Example:
100

            
101
- subscribe `vad/home/balcon/+/south/last`
102
- subscribe `vad/home/balcon/+/south/value`
103
- wait until all required capabilities are satisfied
104
- unsubscribe `vad/home/balcon/+/south/last`
105

            
106
Bootstrap completion SHOULD be defined explicitly.
107

            
108
Good rules:
109

            
110
- all required capabilities have arrived from either `last` or `value`
111
- or the consumer has entered a known good state for its required capability set
112

            
113
Bad rule:
114

            
115
- unsubscribe on first live message only
116

            
117
That rule is incomplete because a single live message does not imply that the consumer has received all required initial state.
118

            
119
---
120

            
121
## Dynamic `mqtt in` Control Messages
122

            
123
For Node-RED dynamic `mqtt in`, control messages SHOULD remain simple.
124

            
125
Preferred forms:
126

            
127
```json
128
{
129
  "action": "subscribe",
130
  "topic": "vad/home/balcon/+/south/last",
131
  "qos": 2,
132
  "rh": 0,
133
  "rap": true
134
}
135
```
136

            
137
```json
138
{
139
  "action": "unsubscribe",
140
  "topic": "vad/home/balcon/+/south/last"
141
}
142
```
143

            
144
Practical recommendation:
145

            
146
- prefer one control message per topic
147
- do not rely on a single multi-topic control message unless runtime behavior is already verified in the target Node-RED version
148

            
149
This keeps behavior easier to debug and makes sidebar traces clearer.
150

            
151
---
152

            
153
## Dedicated MQTT Session Requirement for Retained Bootstrap
154

            
155
This was a critical lesson from the HomeKit consumer adapter.
156

            
157
If a dynamic consumer depends on retained `last` as its cold-start mechanism, it SHOULD use its own MQTT client session.
158

            
159
In Node-RED terms:
160

            
161
- give that dynamic `mqtt in` its own `mqtt-broker` config node
162
- do not share the same broker config with unrelated static or dynamic subscribers when deterministic bootstrap matters
163

            
164
Reason:
165

            
166
- broker config nodes in Node-RED represent shared MQTT client sessions
167
- retained replay behavior is tied to subscribe operations in that session
168
- when static and dynamic subscribers share the same session, retained bootstrap can become non-deterministic from the perspective of one consumer
169

            
170
Observed failure mode:
171

            
172
- a static debug subscriber on `.../last` receives retained messages
173
- a dynamic consumer on the same broker config subscribes later
174
- the consumer does not reliably observe the cold-start retained replay it expects
175
- later live updates re-publish `last`, creating the false impression that only live traffic works
176

            
177
Rule:
178

            
179
- consumers that require deterministic retained bootstrap SHOULD have a dedicated broker config
180

            
181
This is especially important for:
182

            
183
- HomeKit adapters
184
- dashboard cold-start consumers
185
- control nodes that reconstruct state from retained `last`
186

            
187
It is usually not necessary for:
188

            
189
- simple live-only debug subscribers
190
- low-value telemetry observers that do not depend on retained bootstrap semantics
191

            
192
---
193

            
194
## Practical Semantics: Raw vs Derived State
195

            
196
Adapters must be careful not to publish device-generated derived semantics as if they were raw truth.
197

            
198
Example from `ZG-204ZV`:
199

            
200
- raw Zigbee payload contains `presence`
201
- with `fading_time = 0`, that field is effectively raw motion detection
202
- canonical output should therefore be `motion/value`
203
- canonical `presence` should remain reserved for held or higher-level derived occupancy semantics
204

            
205
Rule:
206

            
207
- when a device field is operationally a raw signal, publish it as the raw canonical capability
208
- keep higher-level semantics for fusion, aggregation, or dedicated higher-level adapters
209

            
210
This prevents single-device vendor quirks from leaking into the semantic bus.
211

            
212
---
213

            
214
## Status, Errors, and Flow Observability
215

            
216
Adapter nodes SHOULD expose useful runtime status in the Node-RED editor.
217

            
218
Useful status content:
219

            
220
- detected devices count
221
- processed input count
222
- translated output count
223
- invalid topic count
224
- invalid payload count
225
- dead-letter count
226

            
227
Adapters SHOULD also surface problems through:
228

            
229
- `node.warn` for malformed input and validation failures
230
- `node.error` for internal exceptions
231
- `<site>/sys/adapter/<adapter_id>/error`
232
- `<site>/sys/adapter/<adapter_id>/dlq`
233
- `<site>/sys/adapter/<adapter_id>/stats`
234

            
235
Rule:
236

            
237
- errors should be visible both in operational topics and in Node-RED flow messages
238

            
239
That combination makes debugging much faster than relying on only one channel.
240

            
241
---
242

            
243
## Recommended Debug Setup During Implementation
244

            
245
While implementing a new adapter, use three debug perspectives:
246

            
247
1. raw ingress debug
248

            
249
What the source `mqtt in` actually receives.
250

            
251
2. control-path debug
252

            
253
What control messages are sent to a dynamic `mqtt in`.
254

            
255
3. semantic output debug
256

            
257
What canonical bus publications are emitted.
258

            
259
For consumers using `last` bootstrap, it is useful to have:
260

            
261
- one static debug subscriber on `.../last`
262
- one static debug subscriber on `.../value`
263
- one debug node on the dynamic `mqtt in` output
264
- one debug node on control messages
265

            
266
This makes it possible to distinguish:
267

            
268
- no retained message exists
269
- retained exists but the dynamic subscription did not activate
270
- retained exists but the consumer path is using the wrong MQTT session
271
- parsing or capability mapping is discarding the message
272

            
273
---
274

            
275
## Example Integration: `ZG-204ZV`
276

            
277
### Ingress Adapter
278

            
279
Source:
280

            
281
`zigbee2mqtt/ZG-204ZV/<site>/<location>/<device_id>`
282

            
283
Output:
284

            
285
- `.../value` for live semantic samples
286
- `.../last` retained for latest timestamped sample
287
- `.../meta` retained
288
- `.../availability` retained
289
- `.../sys/adapter/...` for operational topics
290

            
291
### Consumer Adapter
292

            
293
Source:
294

            
295
- `vad/home/<location>/<capability>/<device_id>/last`
296
- `vad/home/<location>/<capability>/<device_id>/value`
297

            
298
Output:
299

            
300
- HomeKit service updates
301
- dynamic `mqtt in` control for bootstrap subscribe/unsubscribe
302

            
303
Lessons learned:
304

            
305
- occupancy may need to be synthesized locally from `motion` and a fading timer
306
- numeric values may arrive as strings and should be normalized defensively
307
- bootstrap completeness must be defined per required capability set
308
- deterministic `last` bootstrap requires a dedicated MQTT session for the dynamic consumer
309

            
310
---
311

            
312
## Summary Rules
313

            
314
- ingress adapters and consumer adapters are different roles and should remain separate
315
- prefer one control message per dynamic subscription topic
316
- keep dynamic `mqtt in` control output as the last output when practical
317
- define bootstrap completeness explicitly
318
- do not unsubscribe from `last` on first live message alone
319
- use dedicated MQTT broker config nodes for dynamic consumers that depend on retained bootstrap
320
- expose operational errors both in `sys` and in Node-RED flow messages
321
- treat raw device-generated signals as raw semantics, not as already-derived business meaning
322