mqtt_bus / addapters.md
Newer Older
868 lines | 24.041kb
Bogdan Timofte authored 2 weeks ago
1
# Adapter
2

            
3
## Definition
4

            
5
An **adapter** is a software component that translates data between an external system or device and the internal MQTT semantic bus used in the infrastructure.
6

            
7
The primary role of the adapter is to **normalize heterogeneous protocols, topic structures, and payload formats** into the canonical structure used by the system.
8

            
9
Adapters do not implement business logic, automation rules, aggregation, or storage. Their responsibility is strictly limited to protocol translation and semantic normalization.
10

            
11
Shared payload, metadata, quality, and operational namespace rules are defined in `mqtt_contract.md` and `sys_bus.md`.
12

            
13
Practical Node-RED conventions and worked examples are documented in `adapter_implementation_examples.md`.
14

            
15
---
16

            
17
## Purpose
18

            
19
In a heterogeneous environment (Zigbee, Tasmota, SNMP, Modbus, MikroTik APIs, custom firmware, etc.), devices publish data using incompatible conventions:
20

            
21
- different topic hierarchies
22
- different payload formats (numeric, text, JSON)
23
- inconsistent naming
24
- missing timestamps
25
- device‑specific semantics
26

            
27
Adapters isolate these differences and expose a **stable internal MQTT bus contract**.
28

            
29
---
30

            
31
## Architectural Position
32

            
33
Adapters operate at the ingress boundary of the system.
34

            
35
Pipeline:
36

            
37
Device / External System
38
    ↓
39
Vendor / Protocol Topics
40
    ↓
41
Adapter
42
    ↓
43
Canonical MQTT Bus
44
    ↓
45
Consumers (HomeKit, historian, automation, analytics)
46

            
47
---
48

            
49
## Responsibilities
50

            
51
An adapter may perform the following transformations:
52

            
53
1. Topic translation to bus contracts
54

            
55
Example:
56

            
57
zigbee2mqtt/bedroom_sensor/temperature
58
        ↓
59
vad/home/bedroom/temperature/bedroom-sensor/value
60

            
61
2. Payload normalization
62

            
63
Examples:
64

            
65
"23.4" → 23.4
66

            
67
{"temperature":23.4}
68
        ↓
69
23.4
70

            
71
3. Timestamp handling
72

            
73
If the device provides an observation timestamp, the adapter must preserve it.
74

            
75
If the device does not provide timestamps, the adapter may attach ingestion time.
76

            
77
4. Unit normalization
78

            
79
Example:
80

            
81
°F → °C
82

            
83
5. Identity normalization
84

            
85
Adapters map vendor identifiers to canonical IDs used by bus contracts.
86

            
87
6. Stream mapping
88

            
89
Adapters must route data to valid streams (`value`, `last`, `set`, `meta`, `availability`) according to bus rules. Legacy `state` and `event` topics remain compatibility-only during migration and SHOULD NOT be introduced by new adapters.
90

            
91
7. Historian policy projection
92

            
93
Adapters should publish enough retained `meta` for historian workers to ingest canonical topics without knowing vendor semantics.
94

            
95
---
96

            
97
## Internal Models vs MQTT Output
98

            
99
Adapters MAY construct richer internal objects during normalization.
100

            
101
Example internal normalization shape:
102

            
103
```json
104
{
105
  "sourcePayload": {
106
    "temperature": 23.6,
107
    "battery": 91
108
  },
109
  "normalizedLocation": "bedroom",
110
  "capability": "temperature",
111
  "deviceId": "bedroom-sensor",
112
  "stream": "value"
113
}
114
```
115

            
116
Rule:
117

            
118
- these structures MUST be ephemeral and limited to the normalization stage
119
- they MUST remain local to the normalization stage and MUST NOT be attached to the `msg` object that continues through the hot-path pipeline
120
- once normalization is complete, the adapter MUST publish MQTT-ready messages as early as possible
121
- before publishing to MQTT, the adapter MUST reduce the message to the canonical MQTT-ready form
122
- consumers on the semantic bus MUST NOT need to understand adapter-specific fields
123

            
124
At the publish boundary, the message SHOULD contain only:
125

            
126
- `msg.topic`
127
- `msg.payload`
128
- `msg.qos` when QoS is not configured statically on the MQTT node
129
- `msg.retain` when retain is not configured statically on the MQTT node
130

            
131
Adapters MUST NOT publish rich internal envelopes such as:
132

            
133
```json
134
{
135
  "topic": "vad/home/bedroom/temperature/bedroom-sensor/value",
136
  "payload": 23.6,
137
  "mapping": {
138
    "source_field": "temperature"
139
  },
140
  "normalizedBus": {
141
    "bus": "home",
142
    "stream": "value"
143
  },
144
  "sourcePayload": {
145
    "temperature": 23.6,
146
    "battery": 91
147
  },
148
  "internalContext": {
149
    "location": "bedroom"
150
  }
151
}
152
```
153

            
154
Those fields may exist inside adapter logic, but MUST be removed before publish.
155

            
156
---
157

            
158
## Fan-Out Pattern
159

            
160
Many ingress protocols emit a single inbound payload containing multiple metrics.
161

            
162
Examples:
163

            
164
- Zigbee2MQTT
165
- Modbus pollers
166
- SNMP collectors
167

            
168
Adapters SHOULD normalize those payloads using fan-out:
169

            
170
1 inbound message
171
        ↓
172
multiple canonical MQTT messages
173

            
174
Example inbound payload:
175

            
176
```json
177
{
178
  "temperature": 23.4,
179
  "humidity": 41
180
}
181
```
182

            
183
Canonical adapter output:
184

            
185
- `vad/home/bedroom/temperature/bedroom-sensor/value` -> `23.4`
186
- `vad/home/bedroom/humidity/bedroom-sensor/value` -> `41`
187

            
188
Rule:
189

            
190
- each emitted message MUST be independent and minimal
191
- fan-out SHOULD produce canonical MQTT-ready outputs directly rather than forwarding one rich `msg` object through multiple downstream stages
192
- metadata for each emitted metric belongs on the corresponding retained `meta` topic
193

            
194
---
195

            
196
## Multi‑Bus Capability Projection
197

            
198
Some physical devices expose multiple capabilities that belong to **different semantic domains**. In such cases an adapter may project different aspects of the same device onto different buses.
199

            
200
A common example is a **smart socket (smart plug)** which provides both:
201

            
202
- a controllable power switch
203
- energy measurement (power or accumulated energy)
204

            
205
These capabilities belong to different semantic models:
206

            
207
- switching is part of the **home automation model**
208
- energy measurement is part of the **energy telemetry model**
209

            
210
An adapter may therefore publish different streams derived from the same device to different buses.
211

            
212
Example:
213

            
214
Home bus (control semantics):
215

            
216
vad/home/living-room/power/tv/value
217
vad/home/living-room/power/tv/set
218

            
219
Energy bus (load telemetry):
220

            
221
vad/energy/load/living-room-entertainment/active_power/value
222
vad/energy/load/living-room-entertainment/energy_total/value
223

            
224
In this situation the adapter performs a **capability projection**, exposing each capability in the semantic domain where it belongs.
225

            
226
This approach prevents the spatial model of the home (`home` bus) from becoming coupled to the electrical topology represented by the `energy` bus, while still allowing a single physical device to participate in both domains.
227

            
228
Rule:
229

            
230
- projection across buses is allowed
231
- semantic duplication inside one bus should be avoided
232
- the same source field must not be published to multiple semantic meanings without an explicit reason
233

            
234
---
235

            
236
## Explicit Non‑Responsibilities
237

            
238
Adapters must NOT:
239

            
240
- implement HomeKit logic
241
- implement automation rules
242
- aggregate multiple sensors
243
- perform anomaly detection
244
- store historical data
245

            
246
These functions belong to other components in the system.
247

            
248
---
249

            
250
## Node-RED Execution Guidelines
251

            
252
Because adapters are implemented in Node-RED, the following constraints apply:
253

            
254
- Node-RED hot paths are not optimized for large per-message object graphs
255
- prefer deterministic `change`/`switch` mapping before custom `function` logic
256
- centralize topic and metric mapping in reusable subflows
257
- minimize per-message allocations on hot paths
258
- avoid large nested objects and rich per-message envelopes
259
- avoid heavy per-message JSON transform on high-rate telemetry
260
- use scalar payload on hot `value` streams and publish metadata on retained `meta`
261
- use retained `last` for cold-start samples that need timestamp/freshness evaluation
262
- keep live `value` streams lightweight and deduplicated where appropriate
263

            
264
---
265

            
266
## Consumer Adapters and Dynamic Subscriptions
267

            
268
Not all adapters are ingress adapters.
269

            
270
In practice, the system also includes consumer adapters that subscribe to canonical bus topics and project them into a downstream model such as HomeKit.
271

            
272
Examples:
273

            
274
- Device -> Protocol Adapter -> MQTT Bus
275
- MQTT Bus -> HomeKit Adapter -> HomeKit
276

            
277
Consumer adapters SHOULD follow these rules:
278

            
279
- consume only canonical bus topics
280
- keep consumer-specific logic out of the ingress adapter
281
- use retained `last` for bootstrap when startup state matters
282
- continue on live `value` after bootstrap
283
- unsubscribe from `last` only after bootstrap completeness is explicitly satisfied
284

            
285
Practical recommendation:
286

            
287
- if a Node-RED adapter node has a dedicated output for controlling a dynamic `mqtt in`, keep that control output as the last output when possible
288

            
289
This keeps semantic outputs stable and reduces rewiring churn.
290

            
291
---
292

            
293
## Dedicated MQTT Session Rule for Retained Bootstrap
294

            
295
If a consumer adapter depends on retained `last` for deterministic cold start, it SHOULD use a dedicated MQTT client session.
296

            
297
In Node-RED this means:
298

            
299
- create a dedicated `mqtt-broker` config node for that dynamic `mqtt in`
300
- do not share the same broker config with unrelated static or dynamic subscribers when retained bootstrap must be isolated
301

            
302
Reason:
303

            
304
- broker config nodes represent shared MQTT client sessions
305
- retained replay behavior is coupled to subscribe operations inside that session
306
- shared sessions make lifecycle-sensitive bootstrap behavior difficult to reason about
307

            
308
Observed failure mode:
309

            
310
- a static subscriber receives retained `last`
311
- a dynamic consumer using the same broker config subscribes later
312
- the consumer does not observe retained bootstrap deterministically
313
- later live updates republish `last`, masking the real issue
314

            
315
Rule:
316

            
317
- live-only telemetry consumers MAY share a broker config
318
- consumers that rely on retained bootstrap SHOULD NOT
319

            
320
---
321

            
322
## Dynamic `mqtt in` Control Recommendations
323

            
324
For Node-RED dynamic `mqtt in`, adapter control messages SHOULD remain simple and explicit.
325

            
326
Preferred pattern:
327

            
328
- one `subscribe` message per topic
329
- one `unsubscribe` message per topic
330

            
331
Example:
332

            
333
```json
334
{
335
  "action": "subscribe",
336
  "topic": "vad/home/balcon/+/south/last",
337
  "qos": 2,
338
  "rh": 0,
339
  "rap": true
340
}
341
```
342

            
343
```json
344
{
345
  "action": "subscribe",
346
  "topic": "vad/home/balcon/+/south/value",
347
  "qos": 2,
348
  "rh": 0,
349
  "rap": true
350
}
351
```
352

            
353
Recommendation:
354

            
355
- prefer separate control messages over multi-topic control payloads unless the exact runtime behavior has been verified on the target Node-RED version
356

            
357
See `adapter_implementation_examples.md` for the full flow pattern and debugging guidance.
358
- allow `last` to update whenever the latest observation timestamp changes, even if the scalar value is unchanged
359
- publish MQTT-ready messages as early as possible once normalization is complete
360
- keep temporary normalization structures in local variables or node-local context, not on forwarded `msg` objects
361
- delete temporary normalization fields before the MQTT publish node
362
- keep MQTT subscriptions narrow (avoid global `#` on hot pipelines)
363
- include error routing for malformed input and unknown mapping cases
364

            
365
Hot-path rule:
366

            
367
- the semantic bus is not a debugging channel
368
- adapters should emit MQTT-ready messages as the semantic boundary
369
- adapter internals belong in transient Node-RED state or under operational `sys` topics, not on bus payloads
370

            
371
Recommended final publish stage:
372

            
373
```javascript
374
return {
375
  topic: normalizedTopic,
376
  payload: normalizedValue
377
};
378
```
379

            
380
If an existing `msg` object must be reused:
381

            
382
```javascript
383
msg.topic = normalizedTopic;
384
msg.payload = normalizedValue;
385

            
386
delete msg.internal;
387
delete msg.internalContext;
388
delete msg.mapping;
389
delete msg.normalizedBus;
390
delete msg.sourcePayload;
391

            
392
return msg;
393
```
394

            
395
Operational requirement:
396

            
397
- malformed or unmappable messages SHOULD be published to `<site>/sys/adapter/<adapter_id>/dlq`
398
- adapter faults SHOULD be published to `<site>/sys/adapter/<adapter_id>/error`
399
- adapter liveness SHOULD be exposed on `<site>/sys/adapter/<adapter_id>/availability`
400
- adapter diagnostics and counters SHOULD be published to `<site>/sys/adapter/<adapter_id>/stats`
401

            
402
See `sys_bus.md` for the shared contract of these operational topics.
403

            
404
Operational recommendation:
405

            
406
- one ingress flow per protocol
407
- one shared normalization subflow per bus contract
408
- one egress flow for publish, with QoS/retain set per stream policy
409

            
410

            
411
## Mapping Registry
412

            
413
Adapters should be driven by declarative mapping data wherever possible.
414

            
415
Recommended mapping fields:
416

            
417
- `source_system`
418
- `source_topic_match`
419
- `source_field`
420
- `target_bus`
421
- `target_location` or `target_entity_id`
422
- `target_capability` or `target_metric`
423
- `target_device_id`
424
- `stream`
425
- `payload_profile`
426
- `unit`
427
- `historian_enabled`
428
- `historian_mode`
429

            
430
Example mapping entry for a Zigbee room sensor:
431

            
432
```json
433
{
434
  "source_system": "zigbee2mqtt",
435
  "source_topic_match": "zigbee2mqtt/bedroom_sensor",
436
  "source_field": "temperature",
437
  "target_bus": "home",
438
  "target_location": "bedroom",
439
  "target_capability": "temperature",
440
  "target_device_id": "bedroom-sensor",
441
  "stream": "value",
442
  "payload_profile": "scalar",
443
  "unit": "C",
444
  "historian_enabled": true,
445
  "historian_mode": "sample"
446
}
447
```
448

            
449
This keeps normalization logic deterministic and reviewable.
450

            
451
---
452

            
453
## Types of Adapters
454

            
455
Adapters are typically organized by protocol or subsystem.
456

            
457
Examples:
458

            
459
zigbee_adapter
460

            
461
Transforms Zigbee2MQTT topics into the canonical bus structure.
462

            
463
network_adapter
464

            
465
Transforms SNMP / router telemetry into the network bus.
466

            
467
energy_adapter
468

            
469
Normalizes inverter, meter, and battery telemetry.
470

            
471
vehicle_adapter
472

            
473
Normalizes EV charger and vehicle telemetry.
474

            
475

            
476
## Zigbee2MQTT Adapter Guidance
477

            
478
Zigbee2MQTT is expected to be one of the first major ingress sources for the new broker, so its projection rules should be explicit.
479

            
480
Source shape:
481

            
482
- base telemetry topic: `zigbee2mqtt/<friendly_name>`
483
- availability topic: `zigbee2mqtt/<friendly_name>/availability`
484
- payload: JSON object with one or more capability fields
485

            
486
Normalization rules:
487

            
488
- one inbound Z2M JSON payload may fan out into multiple canonical MQTT publications
489
- friendly names SHOULD follow the deterministic naming convention defined below so canonical IDs and locations can be derived directly from the topic path
490
- vendor names and IEEE addresses MUST be preserved in `meta.source_ref`, not exposed in canonical topic paths
491
- measurements, booleans, enums, and transition-like signals map to live `value`
492
- adapters SHOULD use `meta.historian.mode` to label whether a `value` stream is semantically a `sample`, `state`, or `event`
493
- adapters SHOULD deduplicate hot `value` publications when the semantic value did not change
494
- adapters SHOULD update retained `last` whenever the latest observed timestamp changes, even if the scalar value is unchanged
495

            
496
Recommended initial Z2M field mapping:
497

            
498
- `temperature` -> `home/.../temperature/.../value`
499
- `humidity` -> `home/.../humidity/.../value`
500
- `pressure` -> `home/.../pressure/.../value`
501
- `illuminance` -> `home/.../illuminance/.../value`
502
- `contact` -> `home/.../contact/.../value`
503
- `occupancy` from PIR devices -> `home/.../motion/.../value`
504
- `presence` from mmWave devices with `fading_time=0` -> `home/.../motion/.../value`
505
- `battery` -> `home/.../battery/.../value`
506
- `state` on smart plugs or switches -> `home/.../power/.../value`
507
- `power`, `energy`, `voltage`, `current` on smart plugs -> `energy/.../.../.../value`
508
- `action` -> `home/.../button/.../value`
509

            
510
mmWave presence handling:
511

            
512
- if a mmWave device emits `presence` while `fading_time=0`, adapters SHOULD treat it as raw motion detection and publish `motion`, not `presence`
513
- device-level `presence` SHOULD usually be derived above the adapter layer through fusion or higher-level logic
514
- adapters MAY publish `presence/.../value` directly only when the device is intentionally configured to expose held presence semantics, for example with non-zero `fading_time`
515

            
516
Fields that SHOULD NOT go to `home` by default:
517

            
518
- `linkquality`
519
- transport diagnostics
520
- adapter-local counters
521

            
522
Those belong on `sys` or a future `network` bus.
523

            
524
## Zigbee2MQTT Topic Structure for Deterministic Adapter Translation
525

            
526
In Zigbee2MQTT the primary telemetry topic structure is:
527

            
528
`zigbee2mqtt/<friendly_name>`
529

            
530
The MQTT prefix is controlled by the Zigbee2MQTT `base_topic` configuration parameter.
531

            
532
Default:
533

            
534
`zigbee2mqtt`
535

            
536
Zigbee2MQTT allows the `/` character inside `friendly_name`, which means the MQTT topic hierarchy can be controlled by the device name itself.
537

            
538
Example:
539

            
540
- friendly name: `kitchen/floor_light`
541
- resulting topic: `zigbee2mqtt/kitchen/floor_light`
542

            
543
This project uses that feature intentionally to encode semantic information directly into the Zigbee2MQTT topic path.
544

            
545
For Zigbee devices in this repository, the `friendly_name` MUST use the following structure:
546

            
547
`<device_type>/<site>/<location>/<device_id>`
548

            
549
Example:
550

            
551
- friendly name: `ZG-204ZV/vad/balcon/south`
552
- resulting topic: `zigbee2mqtt/ZG-204ZV/vad/balcon/south`
553

            
554
Segment meaning:
555

            
556
- `device_type`: hardware model or device class
557
- `site`: canonical site identifier
558
- `location`: canonical room/location identifier
559
- `device_id`: logical endpoint identifier within the location
560

            
561
### Adapter Translation Rationale
562

            
563
Structuring Zigbee2MQTT topics this way allows adapters to perform deterministic translation without lookup tables.
564

            
565
Example inbound topic:
566

            
567
`zigbee2mqtt/ZG-204ZV/vad/balcon/south`
568

            
569
Example payload:
570

            
571
```json
572
{ "illuminance": 704 }
573
```
574

            
575
Adapter output:
576

            
577
`vad/home/balcon/illuminance/south/value`
578

            
579
The adapter only needs to split the topic path and map payload fields to capabilities.
580

            
581
This aligns directly with the home bus contract defined in `home_bus.md`:
582

            
583
`<site>/home/<location>/<capability>/<device_id>/<stream>`
584

            
585
Example:
586

            
587
`vad/home/balcon/illuminance/south/value`
588

            
589
The Zigbee2MQTT topic structure intentionally mirrors the dimensions required by the semantic home bus grammar.
590

            
591
### Naming Rules
592

            
593
- `device_type` should correspond to the hardware model when possible
594
- `location` must match canonical location identifiers used by the home bus
595
- `device_id` must be unique within the location
596
- identifiers must follow MQTT naming rules defined in `mqtt_contract.md`
597
- identifiers should use lowercase kebab-case where possible
598

            
599
This approach removes location mapping tables from adapters, enables deterministic topic parsing, simplifies Node-RED flows, and allows wildcard subscriptions by device type.
600

            
601
Useful subscriptions:
602

            
603
- `zigbee2mqtt/+/+/+/+`
604
- `zigbee2mqtt/ZG-204ZV/#`
605

            
606
## Adapter Auto-Provisioning from Source Topics
607

            
608
The purpose of this section is to define how adapters automatically derive canonical MQTT bus topics from source topics without requiring a static mapping table.
609

            
610
The design goal is to keep adapters deterministic and lightweight while allowing configuration overrides for exceptional cases.
611

            
612
### Principle
613

            
614
Adapters SHOULD attempt to derive semantic dimensions directly from the inbound topic structure.
615

            
616
For Zigbee2MQTT the expected inbound topic format is:
617

            
618
`zigbee2mqtt/<device_type>/<site>/<location>/<device_id>`
619

            
620
Adapters MUST parse the topic segments and attempt to infer:
621

            
622
- source system
623
- device type
624
- site
625
- location
626
- device identifier
627

            
628
These values are then used to construct canonical bus topics.
629

            
630
Example inbound topic:
631

            
632
`zigbee2mqtt/ZG-204ZV/vad/balcon/south`
633

            
634
Parsed dimensions:
635

            
636
- `source = zigbee2mqtt`
637
- `device_type = ZG-204ZV`
638
- `site = vad`
639
- `location = balcon`
640
- `device_id = south`
641

            
642
Example inbound payload:
643

            
644
```json
645
{ "illuminance": 704 }
646
```
647

            
648
Adapter output:
649

            
650
`vad/home/balcon/illuminance/south/value`
651

            
652
### Provisioning Algorithm
653

            
654
Adapters SHOULD follow the following processing order:
655

            
656
1. Parse topic segments.
657
2. Attempt direct semantic mapping.
658
3. Apply configuration overrides.
659
4. Apply default values if required fields are missing.
660

            
661
This ensures deterministic behavior while allowing controlled deviations.
662

            
663
### Configuration Overrides
664

            
665
Adapters MUST support a configuration object allowing explicit overrides.
666

            
667
Overrides allow correcting cases where the inbound topic does not match the canonical semantic model.
668

            
669
Example override configuration:
670

            
671
```json
672
{
673
  "location_map": {
674
    "balcony": "balcon"
675
  },
676
  "bus_override": {
677
    "power": "energy"
678
  },
679
  "device_id_map": {
680
    "south": "radar-south"
681
  }
682
}
683
```
684

            
685
Override categories:
686

            
687
- `location_map`
688
  Maps inbound location identifiers to canonical location identifiers.
689
- `bus_override`
690
  Overrides which semantic bus a capability should be published to.
691
- `device_id_map`
692
  Renames device identifiers when canonical IDs differ from source IDs.
693

            
694
### Default Values
695

            
696
If a semantic dimension cannot be derived from the topic and no override exists, adapters MUST fall back to defaults.
697

            
698
Recommended defaults:
699

            
700
- `site = configured adapter site id`
701
- `bus = home`
702
- `location = unknown`
703
- `device_id = device_type`
704
- `stream = value`
705

            
706
Example fallback result:
707

            
708
`vad/home/unknown/temperature/ZG-204ZV/value`
709

            
710
These defaults ensure the adapter continues operating even when topic information is incomplete.
711

            
712
### Node-RED Implementation Guidance
713

            
714
Adapters implemented in Node-RED SHOULD:
715

            
716
- parse topic segments using deterministic split logic
717
- apply overrides via a centralized configuration object
718
- avoid dynamic lookup tables in hot paths
719
- emit canonical MQTT topics as early as possible
720
- publish malformed or unmappable inputs to:
721

            
722
`<site>/sys/adapter/<adapter_id>/dlq`
723

            
724
Adapter errors SHOULD be published to:
725

            
726
`<site>/sys/adapter/<adapter_id>/error`
727

            
728
This keeps semantic bus traffic deterministic while still exposing operational diagnostics.
729

            
730
### Architectural Rationale
731

            
732
Automatic provisioning from topic structure provides several advantages:
733

            
734
- eliminates large static mapping tables
735
- allows new devices to appear without manual configuration
736
- keeps Node-RED adapter logic simple
737
- keeps canonical bus structure predictable
738
- allows targeted overrides only when necessary
739

            
740
This approach maintains the principle that adapters perform normalization rather than interpretation.
741

            
742

            
743
## Historian Testability
744

            
745
Adapters should support a replay-friendly execution model so historian testing does not depend on live device timing.
746

            
747
Recommended modes:
748

            
749
- live consume from vendor topics
750
- replay recorded vendor fixtures
751
- synthetic generate canonical samples
752

            
753
For historian bootstrap, adapters SHOULD be able to emit:
754

            
755
- retained `last` for streams that must support cold start from the bus
756
- retained `meta`
757
- retained `availability`
758
- realistic live `value` traffic together with retained `last`
759
- dual projection for devices such as smart plugs
760

            
761
---
762

            
763
## Relationship with HomeKit
764

            
765
HomeKit integration is implemented through a **separate adapter layer** that consumes the canonical MQTT bus.
766

            
767
Pipeline:
768

            
769
Device → Protocol Adapter → MQTT Bus → HomeKit Adapter → HomeKit
770

            
771
This separation prevents HomeKit constraints from leaking into protocol translation logic.
772

            
773
---
774

            
775
## Design Principles
776

            
777
Adapters should follow several key principles:
778

            
779
1. Deterministic behavior
780

            
781
The same input must always produce the same output.
782

            
783
2. Stateless processing
784

            
785
Adapters should avoid maintaining internal state unless strictly required.
786

            
787
3. Minimal transformation
788

            
789
Adapters should only normalize data, not reinterpret it.
790

            
791
4. Idempotent operation
792

            
793
Repeated messages should not produce inconsistent system state.
794

            
795
5. Contract-first mapping
796

            
797
Adapters must validate outgoing topics against the target bus grammar.
798

            
799
6. Low-overhead runtime
800

            
801
Adapter behavior should minimize CPU and memory cost in Node-RED hot paths.
802

            
803
7. Operational transparency
804

            
805
Adapter failures must be observable without inspecting raw vendor topics.
806

            
807
---
808

            
809
## Example
810

            
811
Vendor message:
812

            
813
Topic:
814

            
815
zigbee2mqtt/bedroom_sensor
816

            
817
Payload:
818

            
819
{
820
  "temperature": 23.4,
821
  "humidity": 41
822
}
823

            
824
Adapter output:
825

            
826
Topic:
827

            
828
vad/home/bedroom/temperature/bedroom-sensor/value
829

            
830
Payload:
831

            
832
23.4
833

            
834
and
835

            
836
Topic:
837

            
838
vad/home/bedroom/humidity/bedroom-sensor/value
839

            
840
Payload:
841

            
842
41
843

            
844
The final published bus message should be MQTT-ready and minimal, for example:
845

            
846
Topic:
847

            
848
vad/home/balcon/illuminance/radar-south/value
849

            
850
Payload:
851

            
852
1315
853

            
854
It should NOT include diagnostic wrappers, mapping objects, or source payload copies.
855

            
856
If the same source is a smart plug, additional output may also be emitted on the `energy` bus:
857

            
858
Topic:
859

            
860
vad/energy/load/bedroom-heater/active_power/value
861

            
862
Payload:
863

            
864
126.8
865

            
866
---
867

            
868
This abstraction allows the rest of the system to operate independently of the device ecosystem and vendor protocols.