mqtt_bus / energy_bus.md
1 contributor
317 lines | 8.848kb

Energy Bus

Purpose

The energy bus exposes telemetry for the electrical energy domain: production, storage, grid exchange, and electrical load measurement points.

This bus is used for energy accounting and historian ingestion, not for user-facing room automation semantics.

Shared payload, metadata, time, and operational rules are defined in mqtt_contract.md.

Typical systems connected to this bus include:

  • photovoltaic inverters
  • battery systems (BMS and inverter side)
  • UPS systems
  • generators
  • grid meters
  • smart plugs used as electrical measurement points

Scope and Ownership

The energy bus owns telemetry that describes electrical topology and electrical flows.

The home bus owns room-centric control semantics.

Examples:

  • vad/home/living-room/power/tv/value is home automation state
  • vad/energy/load/living-room-entertainment/active_power/value is electrical load telemetry

Rule:

  • if the value is for user control in a room context, publish on home
  • if the value is for electrical accounting, publish on energy

Normative Topic Contract (v1)

All energy topics MUST follow:

<site>/energy/<entity_type>/<entity_id>/<metric>/<stream>

Where:

  • <site>: deployment/site id, for example vad
  • <entity_type>: one of source, storage, grid, load, transfer
  • <entity_id>: stable kebab-case identifier (no spaces)
  • <metric>: canonical metric name in snake_case
  • <stream>: one of value, last, set, meta, availability

Examples:

  • vad/energy/source/pv-roof-1/active_power/value
  • vad/energy/storage/battery-main/soc/value
  • vad/energy/grid/main-meter/import_power/value
  • vad/energy/load/living-room-entertainment/active_power/value
  • vad/energy/transfer/pv-to-battery/power/value

Stream Semantics

  • value: live semantic sample, whether it represents a numeric measurement, a durable state, or a transition-like signal
  • last: retained last-known timestamped sample for startup/bootstrap decisions
  • meta: static or slowly changing metadata
  • availability: online/offline status
  • set: write/request command topic (only where command is supported)

Rules:

  • adapters SHOULD emit live hot-path data only on value
  • adapters SHOULD deduplicate repeated value samples when the semantic value did not change
  • adapters SHOULD publish retained last for the latest known timestamped sample
  • legacy state and event topics SHOULD be treated as compatibility-only during migration and SHOULD NOT be introduced by new adapters

Command safety:

  • set topics MUST NOT be retained
  • command handlers SHOULD confirm via value
  • payload profile and time semantics follow mqtt_contract.md

Payload Contract

To optimize Node-RED flow cost, two payload profiles are defined.

Profile A: Hot Path Scalar (recommended)

For high-frequency telemetry, value payload SHOULD be scalar (number or boolean).

Example:

  • Topic: vad/energy/source/pv-roof-1/active_power/value
  • Payload: 3245.7

Metadata is published separately on retained meta.

Example:

  • Topic: vad/energy/source/pv-roof-1/active_power/meta
  • Payload:
{
    "unit": "W",
    "description": "PV inverter AC active power",
    "source": "modbus_adapter",
    "precision": 0.1
}

Profile B: Envelope JSON (optional)

When observation time or quality must travel with each point:

{
    "value": 3245.7,
    "unit": "W",
    "observed_at": "2026-03-08T10:15:12Z",
    "quality": "good"
}

Recommendation:

  • prefer Profile A for hot telemetry paths
  • use Profile B for low-rate or quality-critical streams
  • use Profile B when source timestamp fidelity matters
  • use last with Profile B and observed_at when startup decisions require freshness evaluation
  • do not repeat metadata or adapter internals in value payloads
  • keep Profile B envelopes small and canonical

Time Semantics

  • observed_at means device observation time, not broker receive time
  • if device timestamp exists, adapter MUST preserve it
  • if missing, adapter MAY use ingestion timestamp and mark degraded quality

Units and Naming

Canonical units SHOULD follow SI conventions:

  • power: W
  • energy: Wh or kWh
  • voltage: V
  • current: A
  • frequency: Hz
  • state of charge: %

Naming rules:

  • entity_id: kebab-case (example battery-main)
  • metric: snake_case (example active_power, import_energy_total)

Metric Classes

The energy bus may carry both measurement-style metrics and counter-style metrics.

Measurement-style examples:

  • active_power
  • voltage
  • current
  • frequency
  • soc
  • charge_power
  • discharge_power

Counter-style cumulative examples:

  • energy_total
  • import_energy_total
  • export_energy_total

MQTT Delivery Policy

Default policy:

  • value: QoS 1, retain false
  • last: QoS 1, retain true
  • meta: QoS 1, retain true
  • availability: QoS 1, retain true (use LWT where available)
  • set: QoS 1, retain false

Cold-start rule:

  • startup consumers SHOULD subscribe to retained last for the latest known measurement
  • last SHOULD include observed_at so consumers can reject stale measurements
  • consumers MAY unsubscribe from last after bootstrap and continue on live value

If uncertain, choose QoS 1.

Node-RED Implementation Optimizations

Because translation is implemented in Node-RED, the contract is optimized for low-overhead flows.

Guidelines:

  • keep canonical topic depth fixed to 6 segments after <site> to simplify wildcard routing
  • avoid per-message heavy JSON transforms on high-rate streams
  • split metadata to retained meta topics to avoid repeated payload bloat
  • publish canonical MQTT-ready messages as early as possible after normalization
  • emit MQTT-ready messages only at the publish boundary
  • do not carry adapter-internal normalization structures on forwarded msg objects
  • discard temporary normalization fields before publish
  • keep high-rate value streams extremely lightweight
  • centralize mapping tables in one function or change node set (device id, metric id, unit)
  • use one normalization subflow reused per adapter/protocol
  • avoid broad # subscriptions in hot paths; use specific wildcard patterns

Suggested Node-RED routing subscriptions:

  • +/energy/+/+/+/value
  • +/energy/+/+/+/last
  • +/energy/+/+/+/meta
  • +/energy/+/+/+/availability

Energy Flow Semantics

Production

Energy generated by a source, usually under entity_type=source.

Examples:

  • active_power
  • energy_total

Storage

Energy held in storage, under entity_type=storage.

Examples:

  • soc
  • charge_power
  • discharge_power

Grid

Import/export metrics at connection points, under entity_type=grid.

Examples:

  • import_power
  • export_power
  • import_energy_total
  • export_energy_total

Load

Electrical load measurement points, under entity_type=load.

Examples:

  • active_power
  • energy_total

Note: this is electrical telemetry semantics, not room-control semantics.

Transfer

Flow between subsystems, under entity_type=transfer.

Examples:

  • pv-to-battery
  • battery-to-home

Historian Relationship

The energy bus is a primary source for historian ingestion, but not all energy metrics use the same persistence path.

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.

For those metrics, workers should map each incoming message to:

  • metric_name
  • device_id
  • value
  • observed_at

device_id recommendation:

  • <entity_type>.<entity_id>

Historian defaults:

  • value streams SHOULD be ingested by default for measurement-style metrics
  • last streams SHOULD NOT be ingested as normal telemetry samples
  • meta.historian.mode SHOULD describe whether a value stream represents sample, state, or event semantics
  • when Profile A scalar is used, observed_at will usually fall back to ingestion time
  • enum-like state values may need explicit encoding before they can be written through the current PostgreSQL historian API
  • 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

Example:

  • Topic: vad/energy/storage/battery-main/soc/value
  • metric_name = soc
  • device_id = storage.battery-main

Design Principles

  • keep semantic ownership clear between buses
  • make energy accounting reconstructable from energy topics
  • optimize for deterministic adapter behavior
  • optimize for low-cost Node-RED translation on high-frequency streams
  • keep contract stable and extensible