mqtt_bus / mqtt_contract.md
Newer Older
420 lines | 14.744kb
Bogdan Timofte authored 2 weeks ago
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.