mqtt_bus / energy_bus.md
Newer Older
317 lines | 8.848kb
Bogdan Timofte authored 2 weeks ago
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