# 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:

```json
{
	"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:

```json
{
	"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
