Showing 10 changed files with 3815 additions and 0 deletions
+5 -0
.gitignore
@@ -0,0 +1,5 @@
1
+.DS_Store
2
+*.swp
3
+*.swo
4
+*~
5
+tdb_ingestion
+275 -0
README.md
@@ -0,0 +1,275 @@
1
+# Home MQTT Semantic Bus
2
+
3
+## Overview
4
+
5
+This project defines the architecture and conventions used to build a semantic MQTT bus for a heterogeneous home infrastructure.
6
+
7
+The environment includes multiple device ecosystems and protocols such as Zigbee, custom ESP firmware, network telemetry, energy systems, and HomeKit integrations. These systems publish data using incompatible topic structures and payload formats.
8
+
9
+The purpose of this repository is to define a canonical internal structure that allows all telemetry, events, and states to be normalized and consumed by multiple systems.
10
+
11
+The MQTT bus acts as the central integration layer between devices and higher-level services.
12
+
13
+Primary documents:
14
+
15
+- `consolidated_spec.md`: consolidated reference linking all specs, decisions, and end-to-end traces
16
+- `mqtt_contract.md`: shared transport, payload, metadata, and historian rules
17
+- `sys_bus.md`: operational namespace for adapters, workers, and infrastructure components
18
+- `home_bus.md`: room-centric semantic bus contract
19
+- `energy_bus.md`: electrical telemetry bus contract
20
+- `addapters.md`: adapter responsibilities and normalization rules
21
+- `adapter_implementation_examples.md`: practical Node-RED adapter patterns, flow integration guidance, and known failure modes
22
+- `historian_worker.md`: historian worker responsibilities for consuming buses and writing to PostgreSQL
23
+- `tdb_ingestion/mqtt_ingestion_api.md`: PostgreSQL historian ingestion API contract for numeric and boolean measurements
24
+- `tdb_ingestion/counter_ingestion_api.md`: counter ingestion API contract (stabilized, not yet implemented)
25
+
26
+---
27
+
28
+## Architectural Model
29
+
30
+The architecture separates five fundamental layers:
31
+
32
+Device Layer
33
+
34
+Devices publish telemetry using vendor-specific protocols and topic structures.
35
+
36
+Examples:
37
+
38
+- Zigbee2MQTT
39
+- Tasmota
40
+- ESP firmware
41
+- SNMP
42
+- MikroTik APIs
43
+- Modbus energy meters
44
+
45
+Protocol Adapter Layer
46
+
47
+Adapters translate vendor-specific topics and payloads into canonical MQTT bus contracts.
48
+
49
+Adapters perform only normalization and protocol translation.
50
+
51
+They must not implement automation logic, aggregation, or persistence.
52
+
53
+MQTT Semantic Bus
54
+
55
+The canonical model is implemented as multiple semantic buses (for example `home`, `energy`, `network`), each with a strict domain contract.
56
+
57
+All higher-level services consume data from this layer.
58
+
59
+The bus is intentionally lightweight: canonical publications must remain minimal, MQTT-ready messages rather than rich adapter envelopes.
60
+
61
+Historian Worker Layer
62
+
63
+Historian persistence is handled by a worker that subscribes to canonical bus topics and writes them into PostgreSQL using the historian ingestion API.
64
+
65
+Consumer Layer
66
+
67
+Multiple systems consume the bus simultaneously:
68
+
69
+- HomeKit integration
70
+- Historian (time-series storage)
71
+- Aggregators
72
+- Automation logic
73
+- Dashboards and monitoring
74
+
75
+Pipeline:
76
+
77
+Device -> Protocol Adapter -> MQTT Bus -> Historian Worker / Other Consumers
78
+
79
+---
80
+
81
+## The Standardization Problem
82
+
83
+IoT ecosystems lack a common telemetry model.
84
+
85
+Different devices publish data using incompatible conventions:
86
+
87
+- inconsistent topic hierarchies
88
+- different payload formats (numeric, text, JSON)
89
+- different naming schemes
90
+- missing timestamps
91
+- device-specific semantics
92
+
93
+This lack of standardization creates several problems:
94
+
95
+- difficult automation
96
+- complex integrations
97
+- duplicated parsing logic
98
+- unreliable historical analysis
99
+
100
+The semantic MQTT bus solves this by enforcing strict internal addressing contracts per bus.
101
+
102
+Adapters isolate vendor inconsistencies and expose normalized data to the rest of the system.
103
+
104
+---
105
+
106
+## Shared Contract Baseline (v1)
107
+
108
+Each bus defines its own topic grammar, but all buses inherit the same shared contract from `mqtt_contract.md`.
109
+
110
+The shared contract defines:
111
+
112
+- the common stream taxonomy (`value`, `last`, `set`, `meta`, `availability`)
113
+- payload profiles (`scalar` and `envelope`)
114
+- retained metadata structure
115
+- time semantics (`observed_at`, `published_at`, `ingested_at`)
116
+- quality states
117
+- operational topics under `<site>/sys/...` with detailed rules in `sys_bus.md`
118
+- historian defaults
119
+
120
+Semantic categories such as `sample`, `state`, and `event` are carried by `meta.historian.mode`, not by introducing separate live stream names in v1.
121
+
122
+This keeps ingestion simple and predictable while allowing low-overhead Node-RED flows.
123
+
124
+---
125
+
126
+## Node-RED Translation Constraints
127
+
128
+Protocol translation is implemented in Node-RED.
129
+
130
+To keep flow cost low and determinism high:
131
+
132
+- keep topic shapes stable and predictable
133
+- avoid expensive JSON transforms in high-rate paths
134
+- publish repeated metadata on retained `meta` topics
135
+- publish canonical MQTT-ready messages as early as possible after normalization
136
+- keep hot-path messages minimal at publish time: `topic`, `payload`, and stream-policy QoS/retain only
137
+- do not carry adapter-internal normalization structures on forwarded `msg` objects
138
+- delete temporary adapter fields before MQTT publish
139
+- do not use semantic bus topics as a debugging channel
140
+- use reusable normalization subflows and centralized mapping tables
141
+- avoid broad `#` subscriptions on high-volume paths
142
+
143
+These constraints are reflected in each bus specification.
144
+
145
+---
146
+
147
+## Operational Separation
148
+
149
+The new broker is treated as a clean semantic boundary.
150
+
151
+Production-facing legacy topics may continue to exist temporarily, but adapters should normalize data into the new broker namespace without leaking old topic structures into the canonical contract.
152
+
153
+The target split is:
154
+
155
+- legacy broker and vendor topics remain compatibility surfaces
156
+- the new broker hosts the semantic buses and adapter operational topics
157
+- historian and future consumers subscribe only to canonical topics
158
+
159
+---
160
+
161
+## Historian Integration
162
+
163
+One of the primary consumers of the bus is the historian.
164
+
165
+The historian records time-series measurements for long-term analysis.
166
+
167
+Typical use cases include:
168
+
169
+- temperature history
170
+- energy production and consumption
171
+- network traffic metrics
172
+- device performance monitoring
173
+
174
+The historian does not communicate directly with devices.
175
+
176
+Instead, it subscribes to normalized bus topics.
177
+
178
+Current ingestion modeling is split in two:
179
+
180
+- numeric and boolean measurements or states go through `tdb_ingestion/mqtt_ingestion_api.md`
181
+- cumulative counters such as `energy_total` follow the separate contract in `tdb_ingestion/counter_ingestion_api.md` (stabilized, not yet implemented)
182
+
183
+Example subscriptions:
184
+
185
+- `+/home/+/+/+/value`
186
+- `+/energy/+/+/+/value`
187
+
188
+This architecture ensures that historical data remains consistent even when devices or protocols change.
189
+
190
+---
191
+
192
+## Project Goals
193
+
194
+The project aims to achieve the following objectives:
195
+
196
+1. Define a stable MQTT semantic architecture for home infrastructure.
197
+2. Decouple device protocols from automation and monitoring systems.
198
+3. Enable multiple independent consumers of telemetry data.
199
+4. Provide consistent topic contracts across heterogeneous systems.
200
+5. Support scalable integration of additional device ecosystems.
201
+6. Enable long-term historical analysis of telemetry.
202
+7. Simplify integration with HomeKit and other user interfaces.
203
+8. Make historian ingestion generic enough to reuse across buses.
204
+9. Keep room for future buses without reworking existing consumers.
205
+
206
+---
207
+
208
+## Core Concepts
209
+
210
+Adapters
211
+
212
+Components that translate between systems and canonical bus contracts.
213
+
214
+In practice there are two useful classes:
215
+
216
+- ingress adapters: vendor/protocol topics -> canonical bus topics
217
+- consumer adapters: canonical bus topics -> downstream consumer models such as HomeKit
218
+
219
+Buses
220
+
221
+Domain-specific normalized telemetry spaces (for example `home`, `energy`, `network`).
222
+
223
+Streams
224
+
225
+Named data flows associated with a capability or metric (`value`, `last`, `set`, `meta`, `availability`).
226
+
227
+Semantic interpretation such as `sample`, `state`, or `event` is carried by retained `meta`, especially `meta.historian.mode`.
228
+
229
+Consumers
230
+
231
+Systems that subscribe to the bus and process the data.
232
+
233
+---
234
+
235
+## Design Principles
236
+
237
+Protocol isolation
238
+
239
+Device ecosystems must not leak their internal topic structure into the system.
240
+
241
+Contract-driven addressing
242
+
243
+All normalized telemetry must follow explicit per-bus topic contracts.
244
+
245
+Loose coupling
246
+
247
+Consumers must not depend on specific device implementations.
248
+
249
+Extensibility
250
+
251
+New buses, locations, devices, and metrics must be easy to integrate.
252
+
253
+Observability
254
+
255
+All telemetry should be recordable by a historian.
256
+
257
+Node-RED efficiency
258
+
259
+Topic and payload design should minimize transformation overhead in Node-RED.
260
+
261
+The MQTT semantic bus is therefore optimized as a low-memory, low-CPU event bus for constrained accessories, SBCs, thin VMs, and high-rate Node-RED flows.
262
+
263
+---
264
+
265
+## Status
266
+
267
+The system is currently being deployed with a new MQTT broker running on:
268
+
269
+`192.168.2.101`
270
+
271
+The legacy broker at:
272
+
273
+`192.168.2.133`
274
+
275
+will be progressively phased out while Node-RED adapters migrate traffic into the canonical bus.
+322 -0
adapter_implementation_examples.md
@@ -0,0 +1,322 @@
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
+
+868 -0
addapters.md
@@ -0,0 +1,868 @@
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.
+693 -0
consolidated_spec.md
@@ -0,0 +1,693 @@
1
+# Consolidated Bus & Data Collection Specification
2
+
3
+Data: 2026-03-20
4
+
5
+Acest document consolidează specificațiile existente într-o referință unică pentru busuri, colectare de date și conformitate cu cerințele historian-ului. Nu înlocuiește documentele sursă, ci le leagă și stabilește deciziile rămase deschise.
6
+
7
+Documentele sursă rămân canonice. Acest document este o hartă de navigare și un registru de decizii.
8
+
9
+---
10
+
11
+## 1. Arhitectura Pipeline-ului
12
+
13
+```
14
+Device / External System
15
+    ↓
16
+Protocol Adapter (Node-RED)
17
+    ↓
18
+Canonical MQTT Bus (home | energy)
19
+    ↓
20
+Historian Worker
21
+    ↓
22
+telemetry.ingest_measurement(...)   ← measurement path (implementat)
23
+telemetry.ingest_counter(...)       ← counter path (contract stabilizat, neimplementat)
24
+    ↓
25
+PostgreSQL Historian
26
+```
27
+
28
+Documente relevante:
29
+- `README.md` — arhitectura generală
30
+- `addapters.md` — responsabilitățile adapterelor
31
+- `historian_worker.md` — responsabilitățile worker-ului
32
+
33
+---
34
+
35
+## 2. Contractul MQTT Partajat
36
+
37
+Definit în `mqtt_contract.md`. Toate busurile moștenesc aceste reguli.
38
+
39
+### 2.1 Namespace
40
+
41
+- Busuri semantice: `<site>/<bus>/...`
42
+- Namespace operațional: `<site>/sys/...`
43
+- `<site>` = site-id stabil, kebab-case (ex: `vad`)
44
+- Busuri v1: `home`, `energy`
45
+
46
+### 2.2 Stream-uri Canonice
47
+
48
+| Stream | Scop | Retain | QoS |
49
+|---|---|---|---|
50
+| `value` | sample live hot-path | false | 1 |
51
+| `last` | ultimul sample cu timestamp, pentru cold-start | true | 1 |
52
+| `set` | comandă/request | false | 1 |
53
+| `meta` | metadata statică sau slow-changing | true | 1 |
54
+| `availability` | online/offline | true (+ LWT) | 1 |
55
+
56
+Reguli:
57
+- adaptoarele emit date live doar pe `value`
58
+- adaptoarele deduplică `value` când valoarea semantică nu s-a schimbat
59
+- adaptoarele actualizează `last` (retained) la fiecare schimbare de timestamp
60
+- `state` și `event` sunt legacy, nu se introduc de adaptoare noi
61
+- `set` nu se reține niciodată
62
+- historian-ul ingestează doar `value`, ignoră `last`, `set`, `meta`, `availability`
63
+
64
+### 2.3 Payload Profiles
65
+
66
+**Profile A — Scalar (default pentru hot paths):**
67
+
68
+```
69
+23.6
70
+41
71
+true
72
+on
73
+```
74
+
75
+- metadata pe `meta` separat (retained)
76
+- `observed_at` = ingestion time (acceptabil pentru Phase 1)
77
+
78
+**Profile B — Envelope JSON (opțional):**
79
+
80
+```json
81
+{
82
+  "value": 23.6,
83
+  "unit": "C",
84
+  "observed_at": "2026-03-08T10:15:12Z",
85
+  "quality": "good"
86
+}
87
+```
88
+
89
+- câmpuri opționale: `published_at`, `source_seq`, `annotations`
90
+- utilizat când fidelitatea timestamp-ului contează sau când quality trebuie propagat per-sample
91
+
92
+### 2.4 Meta Contract
93
+
94
+Fiecare topic retained `meta` descrie familia sibling de `value`/`last`.
95
+
96
+Formă minimă recomandată:
97
+
98
+```json
99
+{
100
+  "schema_ref": "mqbus.home.v1",
101
+  "payload_profile": "scalar",
102
+  "data_type": "number",
103
+  "unit": "C",
104
+  "adapter_id": "z2m-main",
105
+  "source": "zigbee2mqtt",
106
+  "source_ref": "0x00158d0008aa1111",
107
+  "source_topic": "zigbee2mqtt/SENSOR/vad/bedroom/bedroom-sensor",
108
+  "precision": 0.1,
109
+  "historian": {
110
+    "enabled": true,
111
+    "mode": "sample"
112
+  }
113
+}
114
+```
115
+
116
+Câmpuri historian:
117
+- `historian.enabled`: boolean
118
+- `historian.mode`: `sample` | `state` | `event` | `ignore`
119
+- `historian.retention_class`: opțional (`short`, `default`, `long`)
120
+- `historian.sample_period_hint_s`: opțional
121
+
122
+### 2.5 Time Semantics
123
+
124
+Trei timestamp-uri distincte:
125
+
126
+| Timestamp | Semnificație |
127
+|---|---|
128
+| `observed_at` | când sursa a observat/măsurat valoarea |
129
+| `published_at` | când adapterul a publicat mesajul |
130
+| `ingested_at` | când worker-ul a procesat mesajul |
131
+
132
+Reguli:
133
+- dacă sursa are timestamp, adapterul îl păstrează ca `observed_at`
134
+- dacă nu, adapterul omite `observed_at`; worker-ul folosește `ingested_at`
135
+- adaptoarele nu fabrică timestamp-uri
136
+- dacă adapterul estimează timpul, folosește Profile B cu `quality=estimated`
137
+
138
+### 2.6 Quality Model
139
+
140
+Valori: `good`, `estimated`, `degraded`, `stale`, `invalid`
141
+
142
+- `invalid` nu se emite pe bus semantic; merge pe `sys/.../error`
143
+- `quality` se omite doar dacă `good` este implicit
144
+
145
+---
146
+
147
+## 3. Home Bus
148
+
149
+Definit în `home_bus.md`. Bus room-centric pentru senzori, control, automatizare.
150
+
151
+### 3.1 Topic Grammar
152
+
153
+```
154
+<site>/home/<location>/<capability>/<device_id>/<stream>
155
+```
156
+
157
+Exemple:
158
+- `vad/home/bedroom/temperature/bedroom-sensor/value`
159
+- `vad/home/living-room/motion/radar-south/value`
160
+- `vad/home/kitchen/light/ceiling-switch/set`
161
+
162
+### 3.2 Capability Catalog
163
+
164
+**Environmental:** `temperature`, `humidity`, `pressure`, `illuminance`, `co2`, `voc`, `pm25`, `pm10`
165
+
166
+**Presence/Safety:** `motion`, `presence`, `contact`, `water_leak`, `smoke`, `gas`, `tamper`
167
+
168
+**Control:** `light`, `power`, `lock`, `cover_position`, `thermostat_mode`, `target_temperature`, `fan_mode`, `button`
169
+
170
+**Device health:** `battery`, `battery_low`
171
+
172
+Reguli:
173
+- `power` pe `home` doar pentru control semantics (on/off)
174
+- metrici electrice (`active_power`, `energy_total`) pe `energy`
175
+- `linkquality` nu pe `home` (merge pe `sys` sau viitor `network`)
176
+- `presence` = stare derivată de nivel mai înalt; detecția brută mmWave cu `fading_time=0` se publică ca `motion`
177
+
178
+### 3.3 Historian Mapping
179
+
180
+```
181
+metric_name = <capability>
182
+device_id   = <location>.<device_id>
183
+```
184
+
185
+Exemplu:
186
+- Topic: `vad/home/bedroom/temperature/bedroom-sensor/value`
187
+- `metric_name = temperature`
188
+- `device_id = bedroom.bedroom-sensor`
189
+- `value = 23.4` (scalar)
190
+- `observed_at = ingested_at` (Profile A fallback)
191
+
192
+---
193
+
194
+## 4. Energy Bus
195
+
196
+Definit în `energy_bus.md`. Bus pentru topologia electrică: producție, stocare, grid, load.
197
+
198
+### 4.1 Topic Grammar
199
+
200
+```
201
+<site>/energy/<entity_type>/<entity_id>/<metric>/<stream>
202
+```
203
+
204
+Entity types: `source`, `storage`, `grid`, `load`, `transfer`
205
+
206
+Exemple:
207
+- `vad/energy/source/pv-roof-1/active_power/value`
208
+- `vad/energy/storage/battery-main/soc/value`
209
+- `vad/energy/grid/main-meter/import_power/value`
210
+- `vad/energy/load/living-room-tv/active_power/value`
211
+
212
+### 4.2 Metric Classes
213
+
214
+**Measurement-style** (compatibile cu `ingest_measurement()` acum):
215
+
216
+`active_power`, `voltage`, `current`, `frequency`, `soc`, `charge_power`, `discharge_power`
217
+
218
+**Counter-style** (pe bus, dar NU prin measurement API — urmează `ingest_counter()`):
219
+
220
+`energy_total`, `import_energy_total`, `export_energy_total`
221
+
222
+### 4.3 Units Canonice
223
+
224
+| Metric | Unit |
225
+|---|---|
226
+| power | `W` |
227
+| energy | `Wh` sau `kWh` |
228
+| voltage | `V` |
229
+| current | `A` |
230
+| frequency | `Hz` |
231
+| state of charge | `%` |
232
+
233
+### 4.4 Historian Mapping
234
+
235
+```
236
+metric_name = <metric>
237
+device_id   = <entity_type>.<entity_id>
238
+```
239
+
240
+Exemplu:
241
+- Topic: `vad/energy/storage/battery-main/soc/value`
242
+- `metric_name = soc`
243
+- `device_id = storage.battery-main`
244
+
245
+### 4.5 Ownership Rule
246
+
247
+- Dacă valoarea e pentru control utilizator într-o cameră → `home`
248
+- Dacă valoarea e pentru contabilitate electrică → `energy`
249
+- Un device fizic poate proiecta pe ambele busuri (smart plug: `power` pe `home`, `active_power` pe `energy`)
250
+
251
+---
252
+
253
+## 5. Namespace Operațional (sys)
254
+
255
+Definit în `sys_bus.md`. Nu este un bus semantic.
256
+
257
+### 5.1 Topic Grammar
258
+
259
+```
260
+<site>/sys/<producer_kind>/<instance_id>/<stream>
261
+```
262
+
263
+Producer kinds v1: `adapter`, `historian`
264
+
265
+### 5.2 Stream-uri Operaționale
266
+
267
+| Stream | Scop | Retain |
268
+|---|---|---|
269
+| `availability` | liveness-ul componentei | true + LWT |
270
+| `stats` | contori operaționali, snapshot periodic | true |
271
+| `error` | erori structurate pentru operator | false |
272
+| `dlq` | mesaje dead-letter cu context | false |
273
+
274
+Exemple:
275
+- `vad/sys/adapter/z2m-main/availability` → `online`
276
+- `vad/sys/historian/main/error` → `{"code":"unknown_metric",...}`
277
+
278
+---
279
+
280
+## 6. Adaptoare
281
+
282
+Definit în `addapters.md` și `adapter_implementation_examples.md`.
283
+
284
+### 6.1 Responsabilități
285
+
286
+1. Traducere topic-uri din format vendor în bus canonic
287
+2. Normalizare payload (JSON → scalar, conversie unități)
288
+3. Păstrare timestamp sursă (dacă există)
289
+4. Fan-out: 1 mesaj inbound → N publicațiuni canonice
290
+5. Proiecție multi-bus (smart plug → `home` + `energy`)
291
+6. Publicare `meta` retained înaintea traficului live
292
+7. Publicare `availability` retained + LWT
293
+8. Routing erori pe `sys/.../error` și `sys/.../dlq`
294
+
295
+### 6.2 NON-Responsabilități
296
+
297
+- Nu implementează logica HomeKit
298
+- Nu implementează reguli de automatizare
299
+- Nu agregă senzori
300
+- Nu stochează date istorice
301
+- Nu repară mesaje malformed (le expune pe `sys`)
302
+
303
+### 6.3 Publish Boundary
304
+
305
+La publicare, mesajul conține DOAR:
306
+
307
+```javascript
308
+{ topic: normalizedTopic, payload: normalizedValue }
309
+```
310
+
311
+Structurile interne de normalizare se șterg înainte de publish.
312
+
313
+### 6.4 Z2M Topic Convention
314
+
315
+Friendly name: `<device_type>/<site>/<location>/<device_id>`
316
+
317
+Exemplu:
318
+- Z2M: `zigbee2mqtt/ZG-204ZV/vad/balcon/south`
319
+- Canonical: `vad/home/balcon/illuminance/south/value`
320
+
321
+Permite traducere deterministă fără lookup tables.
322
+
323
+### 6.5 Z2M Field Mapping
324
+
325
+| Z2M field | Bus | Canonical capability/metric |
326
+|---|---|---|
327
+| `temperature` | home | `temperature` |
328
+| `humidity` | home | `humidity` |
329
+| `pressure` | home | `pressure` |
330
+| `illuminance` | home | `illuminance` |
331
+| `contact` | home | `contact` |
332
+| `occupancy` (PIR) | home | `motion` |
333
+| `presence` (mmWave, `fading_time=0`) | home | `motion` |
334
+| `battery` | home | `battery` |
335
+| `state` (plug/switch) | home | `power` |
336
+| `action` (remote) | home | `button` |
337
+| `power` (smart plug) | energy | `active_power` |
338
+| `voltage` (smart plug) | energy | `voltage` |
339
+| `current` (smart plug) | energy | `current` |
340
+| `energy` (smart plug) | energy | `energy_total` |
341
+
342
+### 6.6 Mapping Registry
343
+
344
+Adapoarele folosesc configurație declarativă. Câmpuri recomandate:
345
+
346
+`source_system`, `source_topic_match`, `source_field`, `target_bus`, `target_location` / `target_entity_id`, `target_capability` / `target_metric`, `target_device_id`, `stream`, `payload_profile`, `unit`, `historian_enabled`, `historian_mode`
347
+
348
+### 6.7 Auto-Provisioning
349
+
350
+Adapoarele derivă dimensiunile semantice din structura topic-ului Z2M. Override-uri se aplică doar pentru excepții. Default-uri: `bus=home`, `location=unknown`, `stream=value`.
351
+
352
+---
353
+
354
+## 7. Historian Worker
355
+
356
+Definit în `historian_worker.md`.
357
+
358
+### 7.1 Subscription Model
359
+
360
+```
361
++/home/+/+/+/value       ← ingestie primară
362
++/energy/+/+/+/value      ← ingestie primară
363
++/home/+/+/+/meta         ← cache local
364
++/energy/+/+/+/meta       ← cache local
365
++/home/+/+/+/last         ← ignorat pentru ingestie
366
++/energy/+/+/+/last       ← ignorat pentru ingestie
367
+```
368
+
369
+### 7.2 Topic → Historian Mapping (sumar)
370
+
371
+| Bus | `metric_name` | `device_id` |
372
+|---|---|---|
373
+| `home` | `<capability>` | `<location>.<device_id>` |
374
+| `energy` | `<metric>` | `<entity_type>.<entity_id>` |
375
+
376
+### 7.3 Payload Handling
377
+
378
+**Scalar (Profile A):**
379
+- `value` = payload parsat ca number/boolean
380
+- `observed_at` = ingestion time
381
+- `unit` din meta cache
382
+
383
+**Envelope (Profile B):**
384
+- `value` = `payload.value`
385
+- `observed_at` = `payload.observed_at` sau ingestion time
386
+- `unit` = `payload.unit` sau meta cache
387
+
388
+### 7.4 Type Routing
389
+
390
+| Tip payload | Overload DB |
391
+|---|---|
392
+| numeric | `ingest_measurement(..., value::double precision, ...)` |
393
+| boolean | `ingest_measurement(..., value::boolean, ...)` |
394
+| string enum | SKIP (nu se ingestează fără encoding policy explicit) |
395
+| counter cumulative | SKIP measurement path; rutare pe counter pipeline |
396
+
397
+### 7.5 Ordering
398
+
399
+- Serializare scrieri per `(metric_name, device_id)` — obligatoriu
400
+- Paralelism acceptabil între device-uri/metrici diferite
401
+- Retry-urile păstrează ordinea pe stream
402
+
403
+### 7.6 Error Handling
404
+
405
+**Erori permanente** (log + DLQ, nu retry):
406
+- topic incompatibil cu grammar-ul busului
407
+- tip payload incompatibil
408
+- metric necunoscut
409
+- out-of-order measurement
410
+- valoare respinsă de policy
411
+
412
+**Erori tranziente** (retry cu ordine):
413
+- PostgreSQL indisponibil
414
+- deconectare rețea
415
+- deconectare broker temporară
416
+
417
+### 7.7 Meta Cache
418
+
419
+- Cheie: stem topic (fără ultimul segment stream)
420
+- Câmpuri cache: `payload_profile`, `data_type`, `unit`, `historian.enabled`, `historian.mode`, `schema_ref`, `adapter_id`, `source`, `source_ref`
421
+- Missing meta = mod degradat, nu stop
422
+
423
+### 7.8 Deployment
424
+
425
+Model recomandat inițial: un singur worker per site consumând `home` + `energy`.
426
+
427
+---
428
+
429
+## 8. Measurement Ingestion API
430
+
431
+Definit în `tdb_ingestion/mqtt_ingestion_api.md`. Implementat și funcțional.
432
+
433
+### 8.1 Semnătură
434
+
435
+```sql
436
+-- Numeric
437
+SELECT * FROM telemetry.ingest_measurement(
438
+    p_metric_name => $1,
439
+    p_device_id   => $2,
440
+    p_value       => $3::double precision,
441
+    p_observed_at => $4::timestamptz
442
+);
443
+
444
+-- Boolean
445
+SELECT * FROM telemetry.ingest_measurement(
446
+    p_metric_name => $1,
447
+    p_device_id   => $2,
448
+    p_value       => $3::boolean,
449
+    p_observed_at => $4::timestamptz
450
+);
451
+```
452
+
453
+### 8.2 Comportament Automat
454
+
455
+Gestionat de PostgreSQL, worker-ul nu reimplementează:
456
+
457
+- Auto-provisioning device (`ensure_device()`)
458
+- Append-only enforcement (watermark per `metric_name, device_id`)
459
+- Policy lookup din `telemetry.metric_policies`
460
+- Gap detection la sosirea următorului measurement
461
+- Segment splitting (value change, NULL, policy change, gap)
462
+- Tail NULL simulation la query time
463
+
464
+### 8.3 Response Actions
465
+
466
+`opened`, `opened_null`, `extended`, `extended_null`, `split`, `null_to_value`, `value_to_null`, `gap_split`, `gap_to_null`
467
+
468
+Worker-ul le loghează, nu branșează logica pe ele.
469
+
470
+### 8.4 Idempotency
471
+
472
+Limitare curentă: `ingest_measurement()` nu acceptă `idempotency_key`. Replay după network failure ambiguă poate produce eroare out-of-order. Worker-ul tratează aceasta ca duplicate operațional.
473
+
474
+### 8.5 Advisory Locking
475
+
476
+- Un advisory lock per `(metric_name, device_id)` pe tranzacție
477
+- Implementat ca wait (nu NOWAIT)
478
+- Recomandare: writer affinity per `(metric_name, device_id)` via routing determinist
479
+
480
+---
481
+
482
+## 9. Counter Ingestion API
483
+
484
+Definit în `tdb_ingestion/counter_ingestion_api.md`. Contractul este acum materializat în repo și reprezintă ținta de implementare pentru worker și backend.
485
+
486
+### 9.1 Semnătură
487
+
488
+```sql
489
+SELECT * FROM telemetry.ingest_counter(
490
+    p_metric_name     => $1,
491
+    p_device_id       => $2,
492
+    p_counter_value   => $3::numeric,
493
+    p_observed_at     => $4::timestamptz,
494
+    p_source_sequence => $5,  -- nullable
495
+    p_idempotency_key => $6,  -- nullable
496
+    p_snapshot_id     => $7   -- nullable
497
+);
498
+```
499
+
500
+### 9.2 Reguli Core
501
+
502
+- Append-only, ordonat per `(metric_name, device_id)`
503
+- `counter_value` nu acceptă NULL; dacă sursa nu are valoare, observația se omite
504
+- Gap-uri derivate la query time din freshness, nu scrieri sintetice NULL
505
+- Scăderea valorii = boundary semantic (`reset_boundary`, `rollover_boundary`, `invalid_drop`)
506
+- Delta și rate nu traversează boundary-uri de reset/rollover
507
+- Worker-ul rutează aceste metrici pe `ingest_counter()`, nu pe `ingest_measurement()`
508
+
509
+### 9.3 Stream Identity
510
+
511
+```
512
+stream = (metric_name, device_id)
513
+```
514
+
515
+Nu există `stream_id` suplimentar. Ordering, freshness, replay metadata — toate per `(metric_name, device_id)`.
516
+
517
+### 9.4 Reliability Levels
518
+
519
+| Nivel | Cerințe | Garanții |
520
+|---|---|---|
521
+| `degraded / at-most-once` | fără `source_sequence`, fără `idempotency_key` | retry nesigur |
522
+| `recommended / at-least-once` | `source_sequence` sau `idempotency_key` | replay detectabil |
523
+| `stronger replay safety` | ambele | deduplicare explicită |
524
+
525
+### 9.5 Reporting Modes
526
+
527
+| Mode | Semantică |
528
+|---|---|
529
+| `periodic` | cadență așteptată; lipsa update-ului = gap |
530
+| `on_change` | publicare la schimbare; tăcerea nu = delta 0 |
531
+| `hybrid` | on_change + heartbeat periodic |
532
+
533
+### 9.6 Freshness Defaults
534
+
535
+| Reporting mode | Input | Default `stale_after_s` |
536
+|---|---|---|
537
+| `periodic` | `expected_interval_s` | `expected_interval_s * 2` |
538
+| `on_change` | `heartbeat_interval_s` | `heartbeat_interval_s * 2` |
539
+| `hybrid` | `heartbeat_interval_s` | `heartbeat_interval_s * 2` |
540
+
541
+`stale_after_s` explicit în policy overridează default-ul.
542
+
543
+---
544
+
545
+## 10. Decizii Luate
546
+
547
+### 10.1 Closed for Phase 1
548
+
549
+Aceste decizii sunt stabile și se poate implementa pe baza lor.
550
+
551
+| # | Decizie | Status |
552
+|---|---|---|
553
+| 1 | Measurement path: numeric + boolean prin `ingest_measurement()` | Acceptat, implementat |
554
+| 2 | Counter path: separat de measurement, prin `ingest_counter()` | Contract stabilizat, neimplementat |
555
+| 3 | `last` nu se ingestează ca time-series | Închis |
556
+| 4 | Ordering per `(metric_name, device_id)` obligatoriu | Închis |
557
+| 5 | Missing source timestamp → fallback la `ingested_at` | Închis |
558
+| 6 | Missing `meta` = mod degradat, nu hard stop | Închis |
559
+| 7 | String enum states: nu se ingestează fără encoding policy | Închis |
560
+| 8 | Counter `stream_id = (metric_name, device_id)` — fără ID suplimentar | Închis |
561
+| 9 | Counter: NULL never, gaps derivate la query time | Închis |
562
+| 10 | Counter: un model comun cu profile de domeniu, nu separare energy/traffic | Închis |
563
+| 11 | Permanent errors → DLQ + log, nu retry | Închis |
564
+| 12 | Transient errors → retry cu ordine păstrată | Închis |
565
+
566
+### 10.2 Decizii Luate Acum (în acest document)
567
+
568
+| # | Decizie | Alegere | Rațiune |
569
+|---|---|---|---|
570
+| D1 | Snapshot pe bus vs poller→worker | **Snapshot rămâne pe boundary-ul poller→worker.** Bus-ul semantic păstrează publicații per-stream. | Busul semantic e optimizat pentru publicații lightweight per-topic. Snapshot-ul e un artefact de colectare, nu o semantică de bus. Dacă mai târziu se dovedește necesar, se poate evolua contractul explicit. |
571
+| D2 | Freshness defaults | **`stale_after_s = expected_interval * 2`** (conform cu draft-ul existent) | Multiplicatorul 2x din draft e suficient de conservator. Override explicit per policy rămâne disponibil. |
572
+| D3 | Profile energy/traffic counter | **Amânat până la date reale.** | Nu avem network bus; profilele de traffic nu sunt relevante acum. Energy counter profile se definesc când implementăm `ingest_counter()`. |
573
+
574
+---
575
+
576
+## 11. Puncte Deschise (non-blocante)
577
+
578
+Aceste puncte nu blochează Phase 1 dar vor necesita atenție:
579
+
580
+| # | Punct | Când devine relevant |
581
+|---|---|---|
582
+| O1 | `idempotency_key` pe measurement API | Când avem nevoie de replay sigur pe measurement path |
583
+| O2 | Encoding policy pentru string enum states | Când vrem să persistăm HVAC modes sau alte enums |
584
+| O3 | Forma fizică a reset/rollover boundary în counter storage | La implementarea `ingest_counter()` |
585
+| O4 | Batch SQL pentru counter ingestion | Când volumul de counters justifică optimizarea |
586
+| O5 | Completitudine snapshot (cum se declară explicit) | Când se adaugă surse bulk polling |
587
+
588
+---
589
+
590
+## 12. Testing & Validation
591
+
592
+### 12.1 Prima Matrice de Test
593
+
594
+| # | Device | Capabilities | Bus | Historian |
595
+|---|---|---|---|---|
596
+| 1 | Room sensor | `temperature`, `humidity`, `battery` | home | Da (numeric) |
597
+| 2 | Door sensor | `contact`, `battery` | home | Da (boolean + numeric) |
598
+| 3 | Smart plug | `power` (control) | home | Nu (string enum) |
599
+| 3 | Smart plug | `active_power`, `voltage`, `current` | energy | Da (numeric) |
600
+| 3 | Smart plug | `energy_total` | energy | Nu (counter path) |
601
+| 4 | Remote | `button` / `action` | home | Nu (event, skip default) |
602
+
603
+### 12.2 Acceptance Criteria Phase 1
604
+
605
+1. Historian stochează samples `home` + `energy` (measurement) fără parsing vendor topics
606
+2. `value` topics ingestabile doar cu topic parsing + opțional `meta` cache
607
+3. Smart plug proiectat pe ambele busuri fără ambiguitate
608
+4. `meta.historian.mode` respectat (sample/state/event/ignore)
609
+5. Missing timestamps nu blochează ingestia
610
+6. Missing `meta` degradează, nu blochează
611
+7. Erori adapter pe `sys`, nu pe bus semantic
612
+8. Counter totals coexistă pe bus fără a fi confundate cu measurement path
613
+
614
+---
615
+
616
+## 13. Hartă de Navigare a Documentelor
617
+
618
+| Document | Ce conține | Când îl citești |
619
+|---|---|---|
620
+| `mqtt_contract.md` | Contract partajat: payload, meta, time, quality, delivery | Când construiești orice componentă care publică sau consumă de pe bus |
621
+| `home_bus.md` | Topic grammar, capabilities, historian mapping home | Când lucrezi pe adaptoare home sau pe historian worker |
622
+| `energy_bus.md` | Topic grammar, entity types, metrics, historian mapping energy | Când lucrezi pe surse de energie |
623
+| `sys_bus.md` | Namespace operațional, stream-uri sys | Când adaugi observabilitate la adaptoare sau worker |
624
+| `addapters.md` | Responsabilități adapter, fan-out, multi-bus, Z2M convention | Când construiești un adapter |
625
+| `adapter_implementation_examples.md` | Patterns Node-RED, cold-start, failure modes | Când implementezi concret în Node-RED |
626
+| `historian_worker.md` | Subscription, mapping, payload handling, ordering, errors | Când construiești historian worker-ul |
627
+| `tdb_ingestion/mqtt_ingestion_api.md` | API measurement implementat, error handling, ordering | Când scrii cod de ingestie measurement |
628
+| `tdb_ingestion/counter_ingestion_api.md` | Contract counter stabilizat (neimplementat) | Când implementezi counter ingestion |
629
+
630
+---
631
+
632
+## 14. End-to-End Trace: De la Device la PostgreSQL
633
+
634
+### Exemplu 1: Temperatură cameră (measurement, Profile A)
635
+
636
+```
637
+[Z2M Device] zigbee2mqtt/SENSOR/vad/bedroom/bedroom-sensor
638
+  payload: {"temperature": 23.4, "humidity": 41, "battery": 87}
639
+
640
+[Adapter] fan-out → 3 publicațiuni:
641
+  vad/home/bedroom/temperature/bedroom-sensor/value  → 23.4
642
+  vad/home/bedroom/humidity/bedroom-sensor/value     → 41
643
+  vad/home/bedroom/battery/bedroom-sensor/value      → 87
644
+
645
+[Worker] topic parse:
646
+  metric_name = "temperature"
647
+  device_id   = "bedroom.bedroom-sensor"
648
+  value       = 23.4
649
+  observed_at = ingested_at (Profile A, no source timestamp)
650
+
651
+[PostgreSQL]
652
+  SELECT * FROM telemetry.ingest_measurement(
653
+    'temperature', 'bedroom.bedroom-sensor',
654
+    23.4::double precision, now()
655
+  );
656
+  → action: extended
657
+```
658
+
659
+### Exemplu 2: Smart plug dual projection (measurement + counter)
660
+
661
+```
662
+[Z2M Device] zigbee2mqtt/PLUG/vad/living-room/tv-plug
663
+  payload: {"state":"on", "power":126.8, "energy":4.72, "voltage":229.4, "current":0.58}
664
+
665
+[Adapter] fan-out → 5 publicațiuni pe 2 busuri:
666
+  vad/home/living-room/power/tv-plug/value                → "on"          (home, string enum, skip historian)
667
+  vad/energy/load/living-room-tv/active_power/value       → 126.8         (energy, measurement)
668
+  vad/energy/load/living-room-tv/voltage/value            → 229.4         (energy, measurement)
669
+  vad/energy/load/living-room-tv/current/value            → 0.58          (energy, measurement)
670
+  vad/energy/load/living-room-tv/energy_total/value       → 4.72          (energy, counter — skip measurement path)
671
+
672
+[Worker]
673
+  active_power → ingest_measurement('active_power', 'load.living-room-tv', 126.8, now())  ✓
674
+  voltage      → ingest_measurement('voltage', 'load.living-room-tv', 229.4, now())       ✓
675
+  current      → ingest_measurement('current', 'load.living-room-tv', 0.58, now())        ✓
676
+  energy_total → skip measurement path, route to counter pipeline                         ⏳
677
+  "on"         → skip (string enum, no encoding policy)                                   ⊘
678
+```
679
+
680
+### Exemplu 3: Contact sensor (boolean measurement)
681
+
682
+```
683
+[Z2M Device] zigbee2mqtt/SNZB-04P/vad/entrance/front-door
684
+  payload: {"contact": false, "battery": 94}
685
+
686
+[Adapter] fan-out → 2 publicațiuni:
687
+  vad/home/entrance/contact/front-door/value  → false
688
+  vad/home/entrance/battery/front-door/value  → 94
689
+
690
+[Worker]
691
+  contact → ingest_measurement('contact', 'entrance.front-door', false, now())  ✓
692
+  battery → ingest_measurement('battery', 'entrance.front-door', 94, now())     ✓
693
+```
+317 -0
energy_bus.md
@@ -0,0 +1,317 @@
1
+# Energy Bus
2
+
3
+## Purpose
4
+
5
+The `energy` bus exposes telemetry for the electrical energy domain: production, storage, grid exchange, and electrical load measurement points.
6
+
7
+This bus is used for energy accounting and historian ingestion, not for user-facing room automation semantics.
8
+
9
+Shared payload, metadata, time, and operational rules are defined in `mqtt_contract.md`.
10
+
11
+Typical systems connected to this bus include:
12
+
13
+- photovoltaic inverters
14
+- battery systems (BMS and inverter side)
15
+- UPS systems
16
+- generators
17
+- grid meters
18
+- smart plugs used as electrical measurement points
19
+
20
+
21
+## Scope and Ownership
22
+
23
+The `energy` bus owns telemetry that describes electrical topology and electrical flows.
24
+
25
+The `home` bus owns room-centric control semantics.
26
+
27
+Examples:
28
+
29
+- `vad/home/living-room/power/tv/value` is home automation state
30
+- `vad/energy/load/living-room-entertainment/active_power/value` is electrical load telemetry
31
+
32
+Rule:
33
+
34
+- if the value is for user control in a room context, publish on `home`
35
+- if the value is for electrical accounting, publish on `energy`
36
+
37
+
38
+## Normative Topic Contract (v1)
39
+
40
+All energy topics MUST follow:
41
+
42
+`<site>/energy/<entity_type>/<entity_id>/<metric>/<stream>`
43
+
44
+Where:
45
+
46
+- `<site>`: deployment/site id, for example `vad`
47
+- `<entity_type>`: one of `source`, `storage`, `grid`, `load`, `transfer`
48
+- `<entity_id>`: stable kebab-case identifier (no spaces)
49
+- `<metric>`: canonical metric name in snake_case
50
+- `<stream>`: one of `value`, `last`, `set`, `meta`, `availability`
51
+
52
+Examples:
53
+
54
+- `vad/energy/source/pv-roof-1/active_power/value`
55
+- `vad/energy/storage/battery-main/soc/value`
56
+- `vad/energy/grid/main-meter/import_power/value`
57
+- `vad/energy/load/living-room-entertainment/active_power/value`
58
+- `vad/energy/transfer/pv-to-battery/power/value`
59
+
60
+
61
+## Stream Semantics
62
+
63
+- `value`: live semantic sample, whether it represents a numeric measurement, a durable state, or a transition-like signal
64
+- `last`: retained last-known timestamped sample for startup/bootstrap decisions
65
+- `meta`: static or slowly changing metadata
66
+- `availability`: online/offline status
67
+- `set`: write/request command topic (only where command is supported)
68
+
69
+Rules:
70
+
71
+- adapters SHOULD emit live hot-path data only on `value`
72
+- adapters SHOULD deduplicate repeated `value` samples when the semantic value did not change
73
+- adapters SHOULD publish retained `last` for the latest known timestamped sample
74
+- legacy `state` and `event` topics SHOULD be treated as compatibility-only during migration and SHOULD NOT be introduced by new adapters
75
+
76
+Command safety:
77
+
78
+- `set` topics MUST NOT be retained
79
+- command handlers SHOULD confirm via `value`
80
+- payload profile and time semantics follow `mqtt_contract.md`
81
+
82
+
83
+## Payload Contract
84
+
85
+To optimize Node-RED flow cost, two payload profiles are defined.
86
+
87
+### Profile A: Hot Path Scalar (recommended)
88
+
89
+For high-frequency telemetry, `value` payload SHOULD be scalar (`number` or `boolean`).
90
+
91
+Example:
92
+
93
+- Topic: `vad/energy/source/pv-roof-1/active_power/value`
94
+- Payload: `3245.7`
95
+
96
+Metadata is published separately on retained `meta`.
97
+
98
+Example:
99
+
100
+- Topic: `vad/energy/source/pv-roof-1/active_power/meta`
101
+- Payload:
102
+
103
+```json
104
+{
105
+	"unit": "W",
106
+	"description": "PV inverter AC active power",
107
+	"source": "modbus_adapter",
108
+	"precision": 0.1
109
+}
110
+```
111
+
112
+### Profile B: Envelope JSON (optional)
113
+
114
+When observation time or quality must travel with each point:
115
+
116
+```json
117
+{
118
+	"value": 3245.7,
119
+	"unit": "W",
120
+	"observed_at": "2026-03-08T10:15:12Z",
121
+	"quality": "good"
122
+}
123
+```
124
+
125
+Recommendation:
126
+
127
+- prefer Profile A for hot telemetry paths
128
+- use Profile B for low-rate or quality-critical streams
129
+- use Profile B when source timestamp fidelity matters
130
+- use `last` with Profile B and `observed_at` when startup decisions require freshness evaluation
131
+- do not repeat metadata or adapter internals in `value` payloads
132
+- keep Profile B envelopes small and canonical
133
+
134
+
135
+## Time Semantics
136
+
137
+- `observed_at` means device observation time, not broker receive time
138
+- if device timestamp exists, adapter MUST preserve it
139
+- if missing, adapter MAY use ingestion timestamp and mark degraded quality
140
+
141
+
142
+## Units and Naming
143
+
144
+Canonical units SHOULD follow SI conventions:
145
+
146
+- power: `W`
147
+- energy: `Wh` or `kWh`
148
+- voltage: `V`
149
+- current: `A`
150
+- frequency: `Hz`
151
+- state of charge: `%`
152
+
153
+Naming rules:
154
+
155
+- `entity_id`: kebab-case (example `battery-main`)
156
+- `metric`: snake_case (example `active_power`, `import_energy_total`)
157
+
158
+
159
+## Metric Classes
160
+
161
+The `energy` bus may carry both measurement-style metrics and counter-style metrics.
162
+
163
+Measurement-style examples:
164
+
165
+- `active_power`
166
+- `voltage`
167
+- `current`
168
+- `frequency`
169
+- `soc`
170
+- `charge_power`
171
+- `discharge_power`
172
+
173
+Counter-style cumulative examples:
174
+
175
+- `energy_total`
176
+- `import_energy_total`
177
+- `export_energy_total`
178
+
179
+
180
+## MQTT Delivery Policy
181
+
182
+Default policy:
183
+
184
+- `value`: QoS 1, retain false
185
+- `last`: QoS 1, retain true
186
+- `meta`: QoS 1, retain true
187
+- `availability`: QoS 1, retain true (use LWT where available)
188
+- `set`: QoS 1, retain false
189
+
190
+Cold-start rule:
191
+
192
+- startup consumers SHOULD subscribe to retained `last` for the latest known measurement
193
+- `last` SHOULD include `observed_at` so consumers can reject stale measurements
194
+- consumers MAY unsubscribe from `last` after bootstrap and continue on live `value`
195
+
196
+If uncertain, choose QoS 1.
197
+
198
+
199
+## Node-RED Implementation Optimizations
200
+
201
+Because translation is implemented in Node-RED, the contract is optimized for low-overhead flows.
202
+
203
+Guidelines:
204
+
205
+- keep canonical topic depth fixed to 6 segments after `<site>` to simplify wildcard routing
206
+- avoid per-message heavy JSON transforms on high-rate streams
207
+- split metadata to retained `meta` topics to avoid repeated payload bloat
208
+- publish canonical MQTT-ready messages as early as possible after normalization
209
+- emit MQTT-ready messages only at the publish boundary
210
+- do not carry adapter-internal normalization structures on forwarded `msg` objects
211
+- discard temporary normalization fields before publish
212
+- keep high-rate `value` streams extremely lightweight
213
+- centralize mapping tables in one `function` or `change` node set (device id, metric id, unit)
214
+- use one normalization subflow reused per adapter/protocol
215
+- avoid broad `#` subscriptions in hot paths; use specific wildcard patterns
216
+
217
+Suggested Node-RED routing subscriptions:
218
+
219
+- `+/energy/+/+/+/value`
220
+- `+/energy/+/+/+/last`
221
+- `+/energy/+/+/+/meta`
222
+- `+/energy/+/+/+/availability`
223
+
224
+
225
+## Energy Flow Semantics
226
+
227
+### Production
228
+
229
+Energy generated by a source, usually under `entity_type=source`.
230
+
231
+Examples:
232
+
233
+- `active_power`
234
+- `energy_total`
235
+
236
+### Storage
237
+
238
+Energy held in storage, under `entity_type=storage`.
239
+
240
+Examples:
241
+
242
+- `soc`
243
+- `charge_power`
244
+- `discharge_power`
245
+
246
+### Grid
247
+
248
+Import/export metrics at connection points, under `entity_type=grid`.
249
+
250
+Examples:
251
+
252
+- `import_power`
253
+- `export_power`
254
+- `import_energy_total`
255
+- `export_energy_total`
256
+
257
+### Load
258
+
259
+Electrical load measurement points, under `entity_type=load`.
260
+
261
+Examples:
262
+
263
+- `active_power`
264
+- `energy_total`
265
+
266
+Note: this is electrical telemetry semantics, not room-control semantics.
267
+
268
+### Transfer
269
+
270
+Flow between subsystems, under `entity_type=transfer`.
271
+
272
+Examples:
273
+
274
+- `pv-to-battery`
275
+- `battery-to-home`
276
+
277
+
278
+## Historian Relationship
279
+
280
+The `energy` bus is a primary source for historian ingestion, but not all energy metrics use the same persistence path.
281
+
282
+Measurement-style metrics such as `active_power`, `voltage`, `current`, `frequency`, `soc`, `charge_power`, and `discharge_power` are compatible with `tdb_ingestion/mqtt_ingestion_api.md`.
283
+
284
+For those metrics, workers should map each incoming message to:
285
+
286
+- `metric_name`
287
+- `device_id`
288
+- `value`
289
+- `observed_at`
290
+
291
+`device_id` recommendation:
292
+
293
+- `<entity_type>.<entity_id>`
294
+
295
+Historian defaults:
296
+
297
+- `value` streams SHOULD be ingested by default for measurement-style metrics
298
+- `last` streams SHOULD NOT be ingested as normal telemetry samples
299
+- `meta.historian.mode` SHOULD describe whether a `value` stream represents `sample`, `state`, or `event` semantics
300
+- when Profile A scalar is used, `observed_at` will usually fall back to ingestion time
301
+- enum-like state values may need explicit encoding before they can be written through the current PostgreSQL historian API
302
+- counter-style totals such as `energy_total`, `import_energy_total`, and `export_energy_total` SHOULD remain on the `energy` bus but SHOULD follow the separate contract in `tdb_ingestion/counter_ingestion_api.md` rather than being forced through the current measurement API
303
+
304
+Example:
305
+
306
+- Topic: `vad/energy/storage/battery-main/soc/value`
307
+- `metric_name = soc`
308
+- `device_id = storage.battery-main`
309
+
310
+
311
+## Design Principles
312
+
313
+- keep semantic ownership clear between buses
314
+- make energy accounting reconstructable from `energy` topics
315
+- optimize for deterministic adapter behavior
316
+- optimize for low-cost Node-RED translation on high-frequency streams
317
+- keep contract stable and extensible
+429 -0
historian_worker.md
@@ -0,0 +1,429 @@
1
+# Historian Bus Worker
2
+
3
+## Definition
4
+
5
+A historian worker is a consumer component that subscribes to one or more canonical semantic MQTT buses and writes normalized measurements into the PostgreSQL telemetry historian.
6
+
7
+Its role is the inverse of an adapter:
8
+
9
+- adapters translate vendor-specific inputs into canonical bus topics
10
+- historian workers translate canonical bus topics into historian API calls
11
+
12
+The worker is a consumer of the semantic contract, not an owner of it.
13
+
14
+---
15
+
16
+## Purpose
17
+
18
+The worker exists to decouple historian persistence from:
19
+
20
+- vendor topic structures
21
+- protocol-specific payload formats
22
+- source-specific device identities
23
+- source-side timing quirks
24
+
25
+This keeps the historian integration generic enough to ingest any bus that follows the shared MQTT contract.
26
+
27
+---
28
+
29
+## Architectural Position
30
+
31
+The historian worker operates at the egress side of the semantic bus.
32
+
33
+Pipeline:
34
+
35
+Device / External System
36
+    ↓
37
+Protocol Adapter
38
+    ↓
39
+Canonical MQTT Bus
40
+    ↓
41
+Historian Worker
42
+    ↓
43
+`telemetry.ingest_measurement(...)`
44
+    ↓
45
+Historian tables
46
+
47
+The worker consumes canonical MQTT only.
48
+
49
+It MUST NOT subscribe to raw vendor topics such as `zigbee2mqtt/...` for normal historian ingestion.
50
+
51
+---
52
+
53
+## Shared Inputs
54
+
55
+The worker consumes data defined by:
56
+
57
+- `mqtt_contract.md`
58
+- bus-specific contracts such as `home_bus.md` and `energy_bus.md`
59
+- `tdb_ingestion/mqtt_ingestion_api.md`
60
+- `tdb_ingestion/counter_ingestion_api.md` when cumulative counter metrics are present on the bus
61
+
62
+The worker should treat the semantic bus as the source of truth for topic grammar and stream meaning.
63
+
64
+The database API is the source of truth for persistence behavior.
65
+
66
+Boundary decisions are recorded in `consolidated_spec.md` §10.
67
+
68
+---
69
+
70
+## Responsibilities
71
+
72
+The historian worker is responsible for:
73
+
74
+1. Topic subscription
75
+
76
+Subscribe to canonical topics for the target buses.
77
+
78
+2. Topic parsing
79
+
80
+Extract `metric_name`, `device_id`, `stream`, and bus-specific dimensions from the topic.
81
+
82
+3. Payload decoding
83
+
84
+Decode scalar payloads and Profile B envelopes according to `mqtt_contract.md`.
85
+
86
+4. Meta caching
87
+
88
+Consume retained `meta` topics and keep a local cache keyed by topic stem.
89
+
90
+5. Historian policy enforcement
91
+
92
+Respect `meta.historian.enabled` and `meta.historian.mode` when deciding what should be persisted.
93
+
94
+6. Type selection
95
+
96
+Choose the numeric or boolean PostgreSQL overload correctly for measurement-style metrics.
97
+
98
+7. Timestamp resolution
99
+
100
+Use `observed_at` from the envelope when present, otherwise fall back to ingestion time.
101
+
102
+8. Per-stream ordering
103
+
104
+Preserve source order for the same `(metric_name, device_id)` path when sending measurements to PostgreSQL.
105
+
106
+9. Error handling
107
+
108
+Separate permanent message errors from transient infrastructure failures.
109
+
110
+10. Observability
111
+
112
+Emit worker health, counters, and persistence failures on operational topics.
113
+
114
+---
115
+
116
+## Explicit Non-Responsibilities
117
+
118
+The historian worker must NOT:
119
+
120
+- parse vendor-specific topics as part of the normal pipeline
121
+- redefine semantic topic contracts
122
+- invent new device identities outside the bus contract
123
+- perform automation logic
124
+- aggregate unrelated sensors into synthetic measurements
125
+- reimplement historian segment logic already handled in PostgreSQL
126
+- repair malformed semantic messages silently
127
+
128
+If semantic messages are invalid, the worker should surface them operationally and skip persistence.
129
+
130
+---
131
+
132
+## Subscription Model
133
+
134
+Recommended subscriptions per site:
135
+
136
+- `+/home/+/+/+/value`
137
+- `+/home/+/+/+/last`
138
+- `+/energy/+/+/+/value`
139
+- `+/energy/+/+/+/last`
140
+- `+/home/+/+/+/meta`
141
+- `+/energy/+/+/+/meta`
142
+
143
+Optional observability subscriptions:
144
+
145
+- `+/home/+/+/+/availability`
146
+- `+/energy/+/+/+/availability`
147
+- `+/sys/adapter/+/availability`
148
+- `+/sys/adapter/+/error`
149
+
150
+Rules:
151
+
152
+- `meta` SHOULD be subscribed and cached before relying on enrichment
153
+- `set` topics MUST be ignored by the historian worker
154
+- `last` topics MUST be ignored for normal time-series ingestion
155
+- `availability` SHOULD NOT be persisted as telemetry samples unless a separate policy explicitly requires it
156
+
157
+---
158
+
159
+## Topic-to-Historian Mapping
160
+
161
+### Home Bus
162
+
163
+Topic:
164
+
165
+`<site>/home/<location>/<capability>/<device_id>/<stream>`
166
+
167
+Mapped fields:
168
+
169
+- `metric_name = <capability>`
170
+- `device_id = <location>.<device_id>`
171
+
172
+### Energy Bus
173
+
174
+Topic:
175
+
176
+`<site>/energy/<entity_type>/<entity_id>/<metric>/<stream>`
177
+
178
+Mapped fields:
179
+
180
+- `metric_name = <metric>`
181
+- `device_id = <entity_type>.<entity_id>`
182
+
183
+The worker MUST NOT require database-side topic parsing.
184
+
185
+---
186
+
187
+## Payload Handling
188
+
189
+### Scalar Payload
190
+
191
+If the stream uses Profile A:
192
+
193
+- parse the scalar payload as number, boolean, or enum string
194
+- resolve `unit` from cached retained `meta`
195
+- set `observed_at` to ingestion time unless source time is available out of band
196
+
197
+### Envelope Payload
198
+
199
+If the stream uses Profile B:
200
+
201
+- read `value`
202
+- use `observed_at` when present
203
+- use `unit` from payload, falling back to cached `meta.unit`
204
+- use `quality` when present for logs or side metrics
205
+
206
+### Type Compatibility Rule
207
+
208
+The current PostgreSQL historian API directly supports:
209
+
210
+- numeric values
211
+- boolean values
212
+
213
+String enum states are allowed on the semantic bus, but the worker SHOULD skip them unless there is an explicit encoding policy.
214
+
215
+Examples:
216
+
217
+- `open` / `closed` may be mapped to boolean if a metric policy expects that
218
+- `heat` / `cool` / `off` MUST NOT be guessed into arbitrary numeric values
219
+
220
+### Counter Metric Boundary
221
+
222
+Counter-style cumulative metrics such as `energy_total`, `import_energy_total`, `export_energy_total`, `rx_bytes_total`, or `tx_packets_total` are valid semantic bus values, but they are not covered by the current measurement ingestion API.
223
+
224
+Rules:
225
+
226
+- the worker MUST NOT force cumulative counters through `telemetry.ingest_measurement(...)` just because the payload is numeric
227
+- the worker SHOULD route them to the separate counter ingestion path defined in `tdb_ingestion/counter_ingestion_api.md`
228
+- if the counter path is temporarily unavailable, the worker SHOULD skip them explicitly and expose that through operational stats or DLQ
229
+- measurement-style metrics such as `active_power`, `voltage`, `current`, `temperature`, and `soc` continue through the existing measurement API
230
+
231
+---
232
+
233
+## Stream Policy
234
+
235
+Default persistence policy:
236
+
237
+- ingest `value` by default
238
+- ignore `last`
239
+- use `meta.historian.mode` to interpret whether a `value` stream represents `sample`, `state`, or `event` semantics
240
+- ignore `set`
241
+- never treat `meta` itself as a measurement
242
+
243
+Additional rules:
244
+
245
+- if `meta.historian.enabled=false`, the worker MUST skip persistence
246
+- if `meta` is missing, the worker MAY continue with degraded defaults
247
+- missing `meta` MUST NOT block ingestion of otherwise valid numeric or boolean `value` samples
248
+
249
+---
250
+
251
+## Ordering and Delivery
252
+
253
+Ordering is critical because PostgreSQL enforces append-only writes per `(metric_name, device_id)`.
254
+
255
+Rules:
256
+
257
+- the worker MUST serialize writes for the same `(metric_name, device_id)`
258
+- retries MUST preserve order
259
+- the worker SHOULD partition concurrency by `(metric_name, device_id)` or an equivalent stable shard
260
+- out-of-order database errors SHOULD be treated as permanent message errors unless the worker can prove replay ordering was preserved
261
+
262
+Operational implication:
263
+
264
+- broad parallelism is acceptable across distinct devices or metrics
265
+- unordered fan-out for the same logical stream is not acceptable
266
+
267
+---
268
+
269
+## Meta Cache
270
+
271
+The worker should maintain a retained metadata cache.
272
+
273
+Recommended cache key:
274
+
275
+- topic stem without the final stream segment
276
+
277
+Example:
278
+
279
+- topic: `vad/home/bedroom/temperature/bedroom-sensor/meta`
280
+- cache key: `vad/home/bedroom/temperature/bedroom-sensor`
281
+
282
+Recommended cached fields:
283
+
284
+- `payload_profile`
285
+- `data_type`
286
+- `unit`
287
+- `historian.enabled`
288
+- `historian.mode`
289
+- `schema_ref`
290
+- `adapter_id`
291
+- `source`
292
+- `source_ref`
293
+
294
+Rules:
295
+
296
+- cached `meta` SHOULD be replaced atomically when newer retained data arrives
297
+- worker startup SHOULD tolerate receiving live `value` traffic before the corresponding retained `meta`
298
+- if `meta` is deleted, the cache entry SHOULD be removed as well
299
+
300
+---
301
+
302
+## Database Interaction
303
+
304
+The worker writes measurement-style samples through `telemetry.ingest_measurement(...)` as defined in `tdb_ingestion/mqtt_ingestion_api.md`.
305
+
306
+Rules:
307
+
308
+- use the numeric overload for numeric values
309
+- use the boolean overload for boolean values
310
+- type `NULL` explicitly if unknown values are ever supported by bus policy
311
+- log the returned `action` for observability
312
+- do not duplicate historian logic already implemented in PostgreSQL
313
+- do not send counter-style cumulative totals through this API; those follow the separate contract in `tdb_ingestion/counter_ingestion_api.md`
314
+
315
+The worker should assume the database is responsible for:
316
+
317
+- deduplication
318
+- gap detection
319
+- segment splitting
320
+- policy boundary handling
321
+
322
+---
323
+
324
+## Operational Topics
325
+
326
+Shared operational namespace rules are defined in `sys_bus.md`.
327
+
328
+The worker should expose its own operational topics under:
329
+
330
+- `<site>/sys/historian/<worker_id>/availability`
331
+- `<site>/sys/historian/<worker_id>/stats`
332
+- `<site>/sys/historian/<worker_id>/error`
333
+- `<site>/sys/historian/<worker_id>/dlq`
334
+
335
+Recommended uses:
336
+
337
+- `availability`: retained online/offline state
338
+- `stats`: low-rate counters such as ingested messages, skipped samples, and retry counts
339
+- `error`: structured infrastructure or persistence failures
340
+- `dlq`: semantic messages rejected by the worker
341
+
342
+This keeps historian worker observability separate from adapter observability.
343
+
344
+---
345
+
346
+## Failure Handling
347
+
348
+Permanent message failures include:
349
+
350
+- topic does not match a supported bus grammar
351
+- payload type cannot be mapped to the target historian type
352
+- unknown metric in PostgreSQL
353
+- out-of-order measurement
354
+- value violates metric policy
355
+
356
+Transient failures include:
357
+
358
+- PostgreSQL unavailable
359
+- network interruption between worker and database
360
+- temporary broker disconnect
361
+
362
+Rules:
363
+
364
+- permanent message failures SHOULD be logged and dead-lettered
365
+- transient failures SHOULD be retried without reordering per stream
366
+- ambiguous retries that later return out-of-order errors SHOULD be logged as likely duplicates
367
+
368
+---
369
+
370
+## Deployment Model
371
+
372
+A historian worker may be deployed in one of the following ways:
373
+
374
+- one worker for all buses
375
+- one worker per bus
376
+- one worker per site and bus
377
+
378
+Recommended starting model:
379
+
380
+- one worker per site consuming both `home` and `energy`
381
+
382
+This keeps the first implementation simple while preserving the option to split workers later for throughput or failure-domain reasons.
383
+
384
+---
385
+
386
+## Relationship with Adapters
387
+
388
+Adapters and historian workers are symmetrical integration roles.
389
+
390
+Adapter direction:
391
+
392
+- external protocol -> canonical bus
393
+
394
+Historian worker direction:
395
+
396
+- canonical bus -> historian API
397
+
398
+Both should be:
399
+
400
+- contract-driven
401
+- deterministic
402
+- observable
403
+- easy to replay in tests
404
+
405
+Neither should contain business automation logic.
406
+
407
+---
408
+
409
+## Design Principles
410
+
411
+1. Consume canonical topics only
412
+
413
+The worker should depend on semantic contracts, not vendor payloads.
414
+
415
+2. Keep persistence generic
416
+
417
+The same worker model should work for `home`, `energy`, and future buses.
418
+
419
+3. Preserve ordering
420
+
421
+Correct historian writes matter more than maximum ingest parallelism.
422
+
423
+4. Fail visibly
424
+
425
+Bad messages and persistence problems must be observable on operational topics.
426
+
427
+5. Reuse shared contract rules
428
+
429
+The worker should not invent alternate payload or metadata semantics.
+310 -0
home_bus.md
@@ -0,0 +1,310 @@
1
+# Home Bus
2
+
3
+## Purpose
4
+
5
+The `home` bus exposes room-centric and user-facing telemetry, state, and control topics for the living environment.
6
+
7
+Typical systems connected to this bus include:
8
+
9
+- environmental sensors
10
+- lighting and switches
11
+- motion/contact/presence sensors
12
+- thermostats and climate devices
13
+- smart sockets for on/off control semantics
14
+
15
+The `home` bus models living-space behavior, not electrical topology.
16
+
17
+This document defines the `home`-specific topic grammar and semantic ownership.
18
+
19
+Shared rules for payload profiles, metadata, time semantics, quality, and operational topics are defined in `mqtt_contract.md`.
20
+
21
+
22
+## Scope and Ownership
23
+
24
+The `home` bus owns room and automation semantics.
25
+
26
+The `energy` bus owns electrical accounting semantics.
27
+
28
+Examples:
29
+
30
+- `vad/home/living-room/power/tv/value` is room control state
31
+- `vad/home/living-room/power/tv/set` is room control command
32
+- `vad/energy/load/living-room-entertainment/active_power/value` is electrical telemetry
33
+
34
+Rule:
35
+
36
+- publish user-oriented control/state on `home`
37
+- publish electrical measurement and accounting on `energy`
38
+
39
+
40
+## Normative Topic Contract (v1)
41
+
42
+All home topics MUST follow:
43
+
44
+`<site>/home/<location>/<capability>/<device_id>/<stream>`
45
+
46
+Where:
47
+
48
+- `<site>`: deployment/site id, for example `vad`
49
+- `<location>`: normalized area identifier in kebab-case
50
+- `<capability>`: semantic capability in snake_case
51
+- `<device_id>`: stable device/entity identifier in kebab-case
52
+- `<stream>`: one of `value`, `last`, `set`, `meta`, `availability`
53
+
54
+Examples:
55
+
56
+- `vad/home/bedroom/temperature/bedroom-sensor/value`
57
+- `vad/home/living-room/motion/radar-south/value`
58
+- `vad/home/kitchen/light/ceiling-switch/value`
59
+- `vad/home/living-room/power/tv/set`
60
+
61
+
62
+## Canonical Identity Model
63
+
64
+The `home` bus is built around stable room semantics.
65
+
66
+Rules:
67
+
68
+- `<location>` MUST describe the living-space area, not the vendor room name if the two differ
69
+- `<device_id>` MUST identify the logical endpoint exposed to consumers
70
+- one physical device MAY publish multiple capabilities under the same `<device_id>`
71
+- replacing a physical Zigbee sensor SHOULD keep the same canonical `<device_id>` when the logical endpoint remains the same
72
+
73
+Examples:
74
+
75
+- `bedroom-sensor` is a logical endpoint
76
+- `0x00158d0008aa1111` is a vendor identity and belongs in `meta.source_ref`, not in the topic path
77
+
78
+This keeps HomeKit, historian, and automations insulated from vendor identity churn.
79
+
80
+
81
+## Stream Semantics
82
+
83
+- `value`: live semantic sample, whether measurement, held state, or transition notification
84
+- `last`: retained last-known timestamped sample for startup/bootstrap decisions
85
+- `set`: command/request topic for controllable capabilities
86
+- `meta`: static or slowly-changing metadata
87
+- `availability`: online/offline state
88
+
89
+Rules:
90
+
91
+- adapters SHOULD emit live hot-path data only on `value`
92
+- adapters SHOULD deduplicate repeated `value` samples when the semantic value did not change
93
+- adapters SHOULD publish retained `last` for the latest known timestamped sample
94
+- legacy `state` and `event` topics SHOULD be treated as compatibility-only during migration
95
+
96
+Command safety:
97
+
98
+- `set` MUST NOT be retained
99
+- stateful devices SHOULD acknowledge commands via `value`
100
+- payload profile and time semantics follow `mqtt_contract.md`
101
+
102
+
103
+## Payload Contract
104
+
105
+To keep Node-RED flows efficient, two payload profiles are allowed.
106
+
107
+### Profile A: Hot Path Scalar (recommended)
108
+
109
+For frequent updates, payload SHOULD be scalar.
110
+
111
+Examples:
112
+
113
+- `vad/home/bedroom/temperature/bedroom-sensor/value` -> `23.6`
114
+- `vad/home/living-room/motion/radar-south/value` -> `true`
115
+
116
+Metadata is published separately on retained `meta`.
117
+
118
+Example:
119
+
120
+- Topic: `vad/home/bedroom/temperature/bedroom-sensor/meta`
121
+- Payload:
122
+
123
+```json
124
+{
125
+  "unit": "C",
126
+  "source": "zigbee_adapter",
127
+  "precision": 0.1
128
+}
129
+```
130
+
131
+### Profile B: Envelope JSON (optional)
132
+
133
+For streams that need inline quality/timestamp:
134
+
135
+```json
136
+{
137
+  "value": 23.6,
138
+  "unit": "C",
139
+  "observed_at": "2026-03-08T10:15:12Z",
140
+  "quality": "good"
141
+}
142
+```
143
+
144
+Recommendation:
145
+
146
+- prefer Profile A on high-frequency paths
147
+- use Profile B where source quality/time must travel with each sample
148
+- if exact source timestamp must be preserved, use Profile B
149
+- use `last` with Profile B and `observed_at` when control consumers need a startup sample with freshness information
150
+- do not repeat metadata or adapter internals in `value` payloads
151
+- keep Profile B envelopes small and canonical
152
+
153
+
154
+## Capability Catalog (initial)
155
+
156
+Environmental capabilities:
157
+
158
+- `temperature`
159
+- `humidity`
160
+- `pressure`
161
+- `illuminance`
162
+- `co2`
163
+- `voc`
164
+- `pm25`
165
+- `pm10`
166
+
167
+Presence and safety capabilities:
168
+
169
+- `motion`
170
+- `presence`
171
+- `contact`
172
+- `water_leak`
173
+- `smoke`
174
+- `gas`
175
+- `tamper`
176
+
177
+Control and user-facing capabilities:
178
+
179
+- `light`
180
+- `power`
181
+- `lock`
182
+- `cover_position`
183
+- `thermostat_mode`
184
+- `target_temperature`
185
+- `fan_mode`
186
+- `button`
187
+
188
+Device health capabilities that are still user-relevant:
189
+
190
+- `battery`
191
+- `battery_low`
192
+
193
+Rules:
194
+
195
+- use `power` on `home` only for control semantics
196
+- use electrical telemetry such as `active_power` and cumulative counters such as `energy_total` on `energy`
197
+- radio diagnostics such as `linkquality` SHOULD NOT go on `home` by default
198
+- `presence` SHOULD normally be a higher-level derived state, not a raw single-device detection
199
+- raw mmWave detection, including Zigbee `presence` with `fading_time=0`, SHOULD be published as `motion`
200
+
201
+
202
+## MQTT Delivery Policy
203
+
204
+Default policy:
205
+
206
+- `value`: QoS 1, retain false
207
+- `last`: QoS 1, retain true
208
+- `set`: QoS 1, retain false
209
+- `meta`: QoS 1, retain true
210
+- `availability`: QoS 1, retain true (LWT preferred)
211
+
212
+Cold-start rule:
213
+
214
+- control consumers such as thermostats SHOULD subscribe to retained `last` to obtain the latest known sample at startup
215
+- `last` SHOULD include `observed_at` so staleness can be evaluated immediately after subscribe
216
+- consumers MAY unsubscribe from `last` after bootstrap and continue consuming lightweight live updates on `value`
217
+- consumers that depend on deterministic retained bootstrap SHOULD use a dedicated MQTT client session instead of sharing the same broker config with unrelated subscribers
218
+
219
+If uncertain, choose QoS 1.
220
+
221
+
222
+## Node-RED Implementation Optimizations
223
+
224
+Translation is done in Node-RED, so the contract is optimized for low processing overhead.
225
+
226
+Guidelines:
227
+
228
+- keep topic shape stable to simplify wildcard routing and switch nodes
229
+- avoid unnecessary JSON parse/build in high-rate pipelines
230
+- publish device metadata on retained `meta` to avoid payload repetition
231
+- publish canonical MQTT-ready messages as early as possible after normalization
232
+- emit MQTT-ready messages only at the publish boundary
233
+- do not carry adapter-internal normalization structures on forwarded `msg` objects
234
+- discard temporary normalization fields before publish
235
+- keep `value` streams extremely lightweight
236
+- centralize mapping rules (location, capability, device_id) in reusable subflows
237
+- keep one ingress subflow per protocol and one normalization subflow shared by all
238
+- avoid broad `#` subscriptions in hot paths
239
+
240
+Suggested Node-RED subscriptions:
241
+
242
+- `+/home/+/+/+/value`
243
+- `+/home/+/+/+/last`
244
+- `+/home/+/+/+/set`
245
+
246
+
247
+## Interaction with Other Buses
248
+
249
+Cross-bus correlations are implemented by consumers, not by changing bus semantics.
250
+
251
+Examples:
252
+
253
+- combine `home` presence with `energy` load telemetry for occupancy-aware energy decisions
254
+- combine `home` climate data with `network` device state for diagnostics
255
+
256
+
257
+## Historian Relationship
258
+
259
+The `home` bus is a major historian source.
260
+
261
+For ingestion worker compatibility, each message should map to:
262
+
263
+- `metric_name`
264
+- `device_id`
265
+- `value`
266
+- `observed_at`
267
+
268
+`device_id` recommendation:
269
+
270
+- `<location>.<device_id>`
271
+
272
+Historian defaults:
273
+
274
+- `value` streams SHOULD be ingested by default
275
+- `last` streams SHOULD NOT be ingested as normal telemetry samples
276
+- `meta.historian.mode` SHOULD describe whether a `value` stream represents `sample`, `state`, or `event` semantics
277
+- when Profile A scalar is used, `observed_at` will usually fall back to ingestion time
278
+- string enum states are semantically valid, but the current PostgreSQL ingestion API directly supports only numeric and boolean samples
279
+
280
+Example:
281
+
282
+- Topic: `vad/home/bedroom/temperature/bedroom-sensor/value`
283
+- `metric_name = temperature`
284
+- `device_id = bedroom.bedroom-sensor`
285
+
286
+Zigbee2MQTT projection examples:
287
+
288
+- `zigbee2mqtt/bedroom_sensor.temperature` -> `vad/home/bedroom/temperature/bedroom-sensor/value`
289
+- `zigbee2mqtt/front_door_contact.contact` -> `vad/home/entrance/contact/front-door/value`
290
+- `zigbee2mqtt/bedside_remote.action` -> `vad/home/bedroom/button/bedside-remote/value`
291
+
292
+
293
+## Relationship with HomeKit
294
+
295
+The `home` bus is a primary source for HomeKit adapters.
296
+
297
+HomeKit integration remains a consumer layer concern:
298
+
299
+- Device -> Protocol Adapter -> MQTT Bus -> HomeKit Adapter -> HomeKit
300
+
301
+The `home` bus contract SHOULD remain independent from HomeKit-specific modeling constraints.
302
+
303
+
304
+## Design Principles
305
+
306
+- keep room-centric semantics explicit and stable
307
+- separate control semantics (`home`) from electrical accounting (`energy`)
308
+- optimize for deterministic, low-cost Node-RED translation
309
+- keep topic grammar strict and payload overhead low
310
+- keep canonical IDs independent from vendor identifiers
+420 -0
mqtt_contract.md
@@ -0,0 +1,420 @@
1
+# MQTT Shared Contract
2
+
3
+## Purpose
4
+
5
+This document defines the shared contract baseline for all semantic MQTT buses in this repository.
6
+
7
+It exists to keep adapters, historian ingestion, and future buses aligned on the same transport and payload rules.
8
+
9
+Bus-specific documents such as `home_bus.md` and `energy_bus.md` define their own topic grammar, but they inherit the rules from this shared contract.
10
+
11
+The operational namespace under `<site>/sys/...` is specified in more detail by `sys_bus.md`.
12
+
13
+---
14
+
15
+## Namespace Model
16
+
17
+Two top-level namespaces are reserved:
18
+
19
+- semantic bus namespace: `<site>/<bus>/...`
20
+- operational namespace: `<site>/sys/...`
21
+
22
+Examples:
23
+
24
+- `vad/home/bedroom/temperature/bedroom-sensor/value`
25
+- `vad/energy/load/living-room-tv/active_power/value`
26
+- `vad/sys/adapter/z2m-main/error`
27
+
28
+Rules:
29
+
30
+- `<site>` MUST be stable and lowercase kebab-case
31
+- `<bus>` MUST be a reserved bus identifier such as `home`, `energy`, `network`, `compute`, `vehicle`
32
+- `sys` is reserved for operational topics and is not a semantic bus; see `sys_bus.md`
33
+- topic path versioning is intentionally not used in v1 to keep wildcard subscriptions simple
34
+
35
+Versioning rule:
36
+
37
+- breaking contract changes MUST be expressed via document version and `schema_ref`
38
+- existing topic paths MUST remain stable for v1 consumers
39
+
40
+---
41
+
42
+## Shared Streams
43
+
44
+All semantic buses use the same stream taxonomy:
45
+
46
+- `value`: live hot-path semantic sample, whether it represents a measurement, durable state, or transition notification
47
+- `last`: retained last-known timestamped sample used for cold start and freshness evaluation
48
+- `set`: command/request topic
49
+- `meta`: retained metadata for the sibling topic family
50
+- `availability`: online/offline or degraded health signal
51
+
52
+Rules:
53
+
54
+- `set` MUST NOT be retained
55
+- `last` SHOULD be retained
56
+- `meta` SHOULD be retained
57
+- `availability` SHOULD be retained and SHOULD use LWT when supported
58
+- buses MUST NOT invent ad-hoc stream names for v1
59
+- adapters SHOULD publish live semantic data on `value`, not split it across separate `state` and `event` streams
60
+- adapters SHOULD deduplicate hot `value` publications when consecutive samples are semantically identical
61
+- adapters SHOULD update retained `last` whenever the latest observed sample timestamp changes
62
+- legacy `state` and `event` topics SHOULD be treated as compatibility-only during migration and SHOULD NOT be introduced by new adapters
63
+
64
+If a future need appears for diagnostics, replay, dead-letter handling, or adapter metrics, it MUST be modeled under `<site>/sys/...`, not by extending semantic bus streams.
65
+
66
+---
67
+
68
+## Topic Naming Rules
69
+
70
+Common naming rules:
71
+
72
+- identifiers representing physical or logical entities SHOULD use kebab-case
73
+- capability and metric names SHOULD use snake_case
74
+- topic segments MUST be ASCII lowercase
75
+- spaces MUST NOT appear in canonical topics
76
+- vendor-native identifiers MUST NOT leak into semantic topics unless they are the chosen canonical identifier
77
+
78
+Identity rules:
79
+
80
+- canonical IDs MUST be stable across adapter rewrites
81
+- replacing a physical sensor SHOULD NOT force a canonical ID change if the logical endpoint remains the same
82
+- vendor IDs SHOULD be carried in `meta.source_ref`, not in the semantic topic path
83
+
84
+---
85
+
86
+## Lightweight Bus Requirement
87
+
88
+The semantic MQTT bus is a high-efficiency event bus.
89
+
90
+It is NOT:
91
+
92
+- a debugging interface
93
+- a transport for rich adapter envelopes
94
+- a place to repeat internal mapping state on every sample
95
+
96
+Normative rule:
97
+
98
+- semantic bus publications MUST remain minimal and MQTT-ready
99
+- the canonical publication boundary is the MQTT topic plus payload, with QoS and retain determined by stream policy
100
+- adapters MAY build richer internal normalization objects, but those objects MUST be discarded before publish
101
+- adapter-specific fields such as mapping tables, vendor payload snapshots, or internal processing context MUST NOT travel on semantic bus hot paths
102
+
103
+Canonical publication form:
104
+
105
+- topic: `<site>/<bus>/...`
106
+- payload: scalar or small JSON envelope
107
+- qos: `0` or `1`
108
+- retain: according to stream policy
109
+
110
+Discouraged adapter-side publish shape:
111
+
112
+```json
113
+{
114
+  "topic": "vad/home/bedroom/temperature/bedroom-sensor/value",
115
+  "payload": 23.6,
116
+  "homeBus": {
117
+    "location": "bedroom"
118
+  },
119
+  "z2mPayload": {
120
+    "temperature": 23.6,
121
+    "battery": 91
122
+  },
123
+  "mapping": {
124
+    "source_field": "temperature"
125
+  }
126
+}
127
+```
128
+
129
+Those structures may exist inside normalization logic, but MUST be stripped before the MQTT publish boundary.
130
+
131
+Reason:
132
+
133
+- the architecture prioritizes low CPU usage, low memory footprint, predictable Node-RED execution, and compatibility with constrained IoT accessories
134
+- large message objects increase memory pressure
135
+- large nested objects increase garbage collection cost in Node-RED
136
+- constrained accessories, SBCs, and thin VMs benefit from structurally simple bus traffic
137
+
138
+---
139
+
140
+## Payload Profiles
141
+
142
+Two payload profiles are supported across all buses.
143
+
144
+### Profile A: Scalar Payload
145
+
146
+This is the default profile for hot paths.
147
+
148
+Examples:
149
+
150
+- `23.6`
151
+- `41`
152
+- `true`
153
+- `on`
154
+
155
+Profile A requirements:
156
+
157
+- the payload MUST be a scalar: number, boolean, or short string enum
158
+- units and metadata MUST be published on retained `meta`
159
+- if exact source observation time matters, Profile A MUST NOT be used unless broker receive time is acceptable
160
+- Profile A SHOULD be the default for high-rate telemetry and hot `value` streams
161
+- Profile A is the preferred format for live `value` streams consumed by lightweight clients
162
+
163
+Profile A historian rule:
164
+
165
+- historian workers SHOULD use ingestion time as `observed_at` unless an equivalent timestamp is provided out of band
166
+
167
+### Profile B: Envelope JSON
168
+
169
+This profile is used when timestamp, quality, or extra annotations must travel with each sample.
170
+
171
+Canonical shape:
172
+
173
+```json
174
+{
175
+  "value": 23.6,
176
+  "unit": "C",
177
+  "observed_at": "2026-03-08T10:15:12Z",
178
+  "quality": "good"
179
+}
180
+```
181
+
182
+Optional fields:
183
+
184
+- `published_at`: adapter publish time
185
+- `source_seq`: source-side monotonic counter or sequence id
186
+- `annotations`: free-form object for low-rate streams
187
+
188
+Profile B requirements:
189
+
190
+- `value` is REQUIRED
191
+- `observed_at` SHOULD be included when the source provides a timestamp
192
+- `quality` SHOULD be included if the adapter had to estimate or degrade data
193
+- `unit` MAY be omitted for unitless, boolean, or enum values
194
+
195
+Use Profile B when:
196
+
197
+- source timestamp must be preserved
198
+- historian ordering must follow source time, not broker receive time
199
+- per-sample quality matters
200
+- the stream is low-rate enough that JSON overhead is acceptable
201
+- a retained `last` sample is used for startup decisions and consumers must evaluate whether it is still usable
202
+
203
+Profile B restriction:
204
+
205
+- adapters SHOULD avoid envelope JSON on high-rate streams unless there is no acceptable scalar alternative
206
+- Profile B MUST remain a small canonical envelope and MUST NOT be extended into a general-purpose transport for adapter internals
207
+- repeated metadata belongs on retained `meta`, not inside every `value` sample
208
+
209
+---
210
+
211
+## Meta Contract
212
+
213
+Each retained `meta` topic describes the sibling `value` and `last` stream family.
214
+
215
+Minimum recommended shape:
216
+
217
+```json
218
+{
219
+  "schema_ref": "mqbus.home.v1",
220
+  "payload_profile": "scalar",
221
+  "data_type": "number",
222
+  "unit": "C",
223
+  "adapter_id": "z2m-main",
224
+  "source": "zigbee2mqtt",
225
+  "source_ref": "0x00158d0008aa1111",
226
+  "source_topic": "zigbee2mqtt/bedroom_sensor",
227
+  "precision": 0.1,
228
+  "historian": {
229
+    "enabled": true,
230
+    "mode": "sample"
231
+  }
232
+}
233
+```
234
+
235
+Recommended fields:
236
+
237
+- `schema_ref`: stable schema identifier such as `mqbus.home.v1`
238
+- `payload_profile`: `scalar` or `envelope`
239
+- `data_type`: `number`, `boolean`, `string`, or `json`
240
+- `unit`: canonical engineering unit when applicable
241
+- `adapter_id`: canonical adapter instance id
242
+- `source`: source system such as `zigbee2mqtt`, `modbus`, `snmp`
243
+- `source_ref`: vendor or physical device identifier
244
+- `source_topic`: original inbound topic or equivalent source path
245
+- `precision`: numeric precision hint
246
+- `display_name`: human-readable label
247
+- `tags`: optional list for analytics and discovery
248
+- `historian`: ingestion policy object
249
+
250
+Historian metadata contract:
251
+
252
+- `historian.enabled`: boolean
253
+- `historian.mode`: one of `sample`, `state`, `event`, `ignore`
254
+- `historian.retention_class`: optional storage class such as `short`, `default`, `long`
255
+- `historian.sample_period_hint_s`: optional expected cadence
256
+
257
+Rules:
258
+
259
+- `meta` SHOULD be published before live `value` traffic for new streams
260
+- `meta` updates MUST remain backward-compatible for existing consumers during v1
261
+- consumers MUST tolerate missing `meta` and continue with degraded defaults
262
+- repeated descriptive metadata MUST be published on retained `meta`, not repeated on each hot-path `value` publication
263
+
264
+Example:
265
+
266
+- `vad/home/bedroom/temperature/bedroom-sensor/meta` -> `{"unit":"C","precision":0.1,"adapter_id":"z2m-main"}`
267
+- `vad/home/bedroom/temperature/bedroom-sensor/value` -> `23.6`
268
+
269
+---
270
+
271
+## Time Semantics
272
+
273
+The following timestamps are distinct:
274
+
275
+- `observed_at`: when the source system observed or measured the value
276
+- `published_at`: when the adapter normalized and published the message
277
+- `ingested_at`: when the downstream worker processed the message
278
+
279
+Rules:
280
+
281
+- if the source provides a trustworthy timestamp, adapters SHOULD preserve it as `observed_at`
282
+- if the source does not provide a timestamp, adapters MAY omit `observed_at`
283
+- if `observed_at` is omitted, historian workers SHOULD use `ingested_at`
284
+- adapters MUST NOT fabricate source time and mark it as fully trustworthy
285
+- if an adapter estimates time, it SHOULD use Profile B with `quality=estimated`
286
+
287
+This rule is the key tradeoff between low-overhead scalar payloads and strict time fidelity.
288
+
289
+---
290
+
291
+## Quality Model
292
+
293
+The following quality values are recommended:
294
+
295
+- `good`: trusted value from source
296
+- `estimated`: value or timestamp estimated by adapter
297
+- `degraded`: source known to be unstable or partially invalid
298
+- `stale`: source not updated within expected cadence
299
+- `invalid`: malformed or failed validation
300
+
301
+Rules:
302
+
303
+- omit `quality` only when `good` is implied
304
+- `invalid` payloads SHOULD NOT be emitted on semantic bus topics
305
+- invalid or unmappable messages SHOULD be routed to operational error topics under `sys`
306
+
307
+---
308
+
309
+## Delivery Policy
310
+
311
+Shared defaults:
312
+
313
+- `value`: QoS 1, retain false unless a bus-specific contract explicitly requires otherwise
314
+- `last`: QoS 1, retain true
315
+- `set`: QoS 1, retain false
316
+- `meta`: QoS 1, retain true
317
+- `availability`: QoS 1, retain true
318
+
319
+Additional rules:
320
+
321
+- `value` is the live stream and SHOULD remain lightweight
322
+- `last` is the cold-start bootstrap mechanism for latest known measurements on the semantic bus
323
+- `last` SHOULD use Profile B and include `observed_at`
324
+- consumers MUST treat a retained `last` sample as the latest known observation, not as proof of freshness
325
+- if freshness matters, consumers SHOULD evaluate `observed_at`, `availability`, and expected cadence from `meta`
326
+- adapters MAY deduplicate `value` publications, but `last` SHOULD be updated whenever the latest observed sample timestamp changes
327
+- command acknowledgements SHOULD be emitted on normalized `value`, not by retaining `set`
328
+- late joiners MUST be able to reconstruct stream meaning and last known sample from retained `meta`, retained `last`, and retained `availability`
329
+- consumers that require deterministic retained bootstrap SHOULD use a dedicated MQTT client session rather than sharing a session with unrelated subscribers on the same broker config
330
+
331
+---
332
+
333
+## Command Envelope Guidance
334
+
335
+Simple `set` commands may use scalar payloads:
336
+
337
+- `on`
338
+- `off`
339
+- `21.5`
340
+
341
+If correlation or richer semantics are required, a JSON envelope is allowed:
342
+
343
+```json
344
+{
345
+  "value": "on",
346
+  "request_id": "01HRN8KZQ2D7P0S4M6B4CJ3M8Y",
347
+  "requested_at": "2026-03-08T10:20:00Z"
348
+}
349
+```
350
+
351
+Rules:
352
+
353
+- command topics MUST remain bus-specific and capability-specific
354
+- acknowledgements SHOULD be published separately on normalized `value`
355
+- adapters SHOULD avoid command-side business logic
356
+
357
+---
358
+
359
+## Operational Namespace
360
+
361
+Operational topics are for adapter health, replay control, and malformed input handling.
362
+
363
+Detailed operational namespace rules are defined in `sys_bus.md`.
364
+
365
+Recommended topics:
366
+
367
+- `<site>/sys/adapter/<adapter_id>/availability`
368
+- `<site>/sys/adapter/<adapter_id>/stats`
369
+- `<site>/sys/adapter/<adapter_id>/error`
370
+- `<site>/sys/adapter/<adapter_id>/dlq`
371
+
372
+Recommended uses:
373
+
374
+- `availability`: retained adapter liveness
375
+- `stats`: low-rate counters such as published points or dropped messages
376
+- `error`: structured adapter errors that deserve operator attention
377
+- `dlq`: dead-letter payloads for messages that could not be normalized
378
+
379
+This keeps operational concerns separate from semantic buses and avoids polluting historian input.
380
+
381
+Debugging, replay diagnostics, and adapter-internal observability MUST be published under `<site>/sys/...`, not embedded into semantic bus payloads.
382
+
383
+---
384
+
385
+## Retained Message Lifecycle
386
+
387
+Retained topics are part of the contract and require explicit lifecycle handling.
388
+
389
+Rules:
390
+
391
+- `meta`, `last`, and `availability` SHOULD be retained when they represent current truth
392
+- when a retained topic must be deleted, publish a zero-byte retained message to the same topic
393
+- adapters SHOULD clear retained topics when an entity is decommissioned or renamed
394
+- consumers MUST tolerate retained data arriving before or after live traffic
395
+
396
+---
397
+
398
+## Historian Ingestion Defaults
399
+
400
+Historian workers SHOULD apply the following defaults:
401
+
402
+- ingest `value` streams by default
403
+- interpret `meta.historian.mode` as the semantic category of the `value` stream, for example `sample`, `state`, or `event`
404
+- ignore `last`, `set`, `meta`, and `availability` as time-series samples
405
+
406
+Current PostgreSQL historian compatibility:
407
+
408
+- numeric and boolean samples are directly compatible with `tdb_ingestion/mqtt_ingestion_api.md`
409
+- string enum states are valid on the semantic bus, but SHOULD stay out of historian until an explicit encoding policy exists
410
+- counter-style cumulative metrics such as `energy_total`, `*_bytes_total`, and `*_packets_total` are valid bus metrics, but the current measurement API does not define their storage semantics; see `tdb_ingestion/counter_ingestion_api.md`
411
+- if enum state ingestion is needed, the worker MUST map it to an agreed numeric or boolean representation before calling PostgreSQL
412
+
413
+Default field mapping:
414
+
415
+- `value` from payload or envelope
416
+- `observed_at` from envelope if present, otherwise ingestion time
417
+- `unit` from envelope if present, otherwise cached retained `meta.unit`
418
+- `quality` from envelope if present, otherwise `good`
419
+
420
+This allows historian workers to stay generic while bus contracts remain strict.
+176 -0
sys_bus.md
@@ -0,0 +1,176 @@
1
+# Sys Operational Namespace
2
+
3
+## Purpose
4
+
5
+This document defines the shared operational MQTT namespace under `<site>/sys/...`.
6
+
7
+It is used for adapter, worker, bridge, and infrastructure observability.
8
+
9
+`sys` is not a semantic bus. It exists to expose component health, counters, errors, and rejected messages without polluting domain buses such as `home` or `energy`.
10
+
11
+---
12
+
13
+## Namespace Model
14
+
15
+Canonical shape:
16
+
17
+`<site>/sys/<producer_kind>/<instance_id>/<stream>`
18
+
19
+Examples:
20
+
21
+- `vad/sys/adapter/z2m-zg-204zv/availability`
22
+- `vad/sys/adapter/z2m-zg-204zv/stats`
23
+- `vad/sys/historian/main/error`
24
+- `vad/sys/historian/main/dlq`
25
+
26
+Rules:
27
+
28
+- `<site>` MUST be stable lowercase kebab-case
29
+- `<producer_kind>` identifies the emitting software component, not a device capability
30
+- `<instance_id>` MUST identify a stable logical instance of that component
31
+- `sys` MUST NOT be used for room, device, or capability telemetry
32
+- semantic topics such as `<site>/home/...` and `<site>/energy/...` MUST remain separate from `sys`
33
+
34
+Common `producer_kind` values in v1:
35
+
36
+- `adapter`
37
+- `historian`
38
+
39
+Additional producer kinds MAY be introduced later, but they SHOULD follow the same operational topic model.
40
+
41
+---
42
+
43
+## Streams
44
+
45
+The `sys` namespace uses operational streams, not the semantic stream taxonomy from domain buses.
46
+
47
+Supported v1 streams:
48
+
49
+- `availability`
50
+- `stats`
51
+- `error`
52
+- `dlq`
53
+
54
+### `availability`
55
+
56
+Meaning:
57
+
58
+- liveness of the emitting component instance
59
+- whether the adapter or worker itself is online
60
+
61
+Typical payloads:
62
+
63
+- `online`
64
+- `offline`
65
+- optionally `degraded` for components that expose a degraded mode
66
+
67
+Policy:
68
+
69
+- QoS 1
70
+- retain true
71
+- SHOULD use LWT when supported by the MQTT client
72
+
73
+### `stats`
74
+
75
+Meaning:
76
+
77
+- low-rate counters or snapshots describing the current operational state of the component
78
+
79
+Typical payload shape:
80
+
81
+```json
82
+{
83
+  "processed_inputs": 1824,
84
+  "translated_messages": 9241,
85
+  "errors": 2,
86
+  "dlq": 1
87
+}
88
+```
89
+
90
+Policy:
91
+
92
+- QoS 1
93
+- retain true when the message represents the latest snapshot
94
+- publish at a low rate, not on every hot-path sample
95
+
96
+### `error`
97
+
98
+Meaning:
99
+
100
+- structured operator-visible errors
101
+- faults that deserve attention but do not require embedding diagnostics into semantic bus payloads
102
+
103
+Typical payload shape:
104
+
105
+```json
106
+{
107
+  "code": "invalid_topic",
108
+  "reason": "Topic must start with zigbee2mqtt/ZG-204ZV",
109
+  "source_topic": "zigbee2mqtt/bad/topic",
110
+  "adapter_id": "z2m-zg-204zv"
111
+}
112
+```
113
+
114
+Policy:
115
+
116
+- QoS 1
117
+- retain false by default
118
+- SHOULD remain structured and compact
119
+
120
+### `dlq`
121
+
122
+Meaning:
123
+
124
+- dead-letter payloads for messages that could not be normalized or safely processed
125
+
126
+Typical payload shape:
127
+
128
+```json
129
+{
130
+  "code": "payload_not_object",
131
+  "source_topic": "zigbee2mqtt/ZG-204ZV/vad/balcon/south",
132
+  "payload": "offline"
133
+}
134
+```
135
+
136
+Policy:
137
+
138
+- QoS 1
139
+- retain false
140
+- SHOULD carry enough context to reproduce or inspect the rejected message
141
+
142
+---
143
+
144
+## Availability Semantics
145
+
146
+Operational availability and semantic availability are different signals.
147
+
148
+Examples:
149
+
150
+- `vad/sys/adapter/z2m-zg-204zv/availability` means the adapter is running
151
+- `vad/home/balcon/motion/south/availability` means the canonical endpoint is available to consumers
152
+
153
+These topics are complementary, not duplicates.
154
+
155
+Rules:
156
+
157
+- `sys/.../availability` describes component health
158
+- semantic `.../availability` describes endpoint availability on a domain bus
159
+
160
+---
161
+
162
+## Consumer Guidance
163
+
164
+- historians SHOULD NOT treat `sys` topics as normal semantic measurements by default
165
+- dashboards, alerting, and operator tooling SHOULD subscribe to `sys`
166
+- malformed input, normalization failures, and adapter faults SHOULD be routed to `sys`, not embedded into semantic payloads
167
+- consumers SHOULD expect `stats` to be snapshots and `error`/`dlq` to be transient diagnostics
168
+
169
+---
170
+
171
+## Relationship to Other Documents
172
+
173
+- shared transport and payload rules: `mqtt_contract.md`
174
+- adapter operational responsibilities: `addapters.md`
175
+- historian worker operational topics: `historian_worker.md`
176
+- semantic endpoint contracts: `home_bus.md`, `energy_bus.md`