Data flows: Energy and automations

Part of Data flows in the Energy Node system. The chapter numbers follow the overview.

7. Energy balance

The balance is derived data. Entities get roles, and the roles produce a balance object that is available over HTTP and over MQTT.

flowchart LR
    REG[("registry<br/>entity values")] --> RES["energy.Resolver<br/>role per entity"]
    EJ[("energy.json<br/>assignments + interpretation")] --> RES
    RES --> AGG["energy.Aggregate"]
    AGG --> BAL["Balance:<br/>pv, grid_import/export,<br/>battery_charge/discharge,<br/>load_total, wallbox, heat_pump,<br/>battery_soc, self-sufficiency"]
    BAL --> HTTP["GET /api/v1/energy"]
    BAL --> MQTT["publish every 10 s, retained<br/>outstation/energy_node/energy/balance"]
    MQTT --> AUTO["automation_mqtt.py<br/>balance_threshold condition"]
    MQTT --> HAS["Home Assistant sensors<br/>of the device Energy Node"]
    HTTP --> UI["energy-flow chart"]
    BAL --> EST["energystats<br/>hourly kWh (§13)"]

The payload is {"at": <unix s>, "balance": {…}, "interpretation": {…}}. balance.categories holds one value in watts per custom category, keyed by the category ID. Consumers are never negative, while producers and storage keep their raw sign. The seven energy sensors from §3 each pick one field of balance, and Home Assistant records them. These recorded values are what the companion later supplies back (§14).

The automation service takes the balance as finished numbers and computes nothing itself. If the balance is older than balance_max_age_s (default 30 s), rules based on it stop firing.

8. Automations

sequenceDiagram
    participant UI as automations editor
    participant A as httpapi
    participant F as automation_rules.json
    participant M as Mosquitto
    participant S as automation_mqtt.py

    UI->>A: PUT /api/v1/configurations/automation_rules<br/>(role automations + CSRF)
    A->>F: validate, revision, write atomically
    A->>M: publish outstation/automation/config/reload
    M->>S: reload
    S->>S: reload rules, re-subscribe topics

    Note over S,M: Running operation
    M-->>S: balance + subscribed topics
    S->>S: check conditions (with hold times)
    S->>M: publish action (allowed prefixes only)

    Note over UI,S: Single test
    UI->>A: POST /api/v1/automations/test<br/>with rule_id and action_index
    A->>M: publish outstation/automation/test/set
    M->>S: test job
    S->>M: publish outstation/automation/test/result

The condition types are balance_threshold, battery_soc, topic_value, time_window, sun_window and entity_value. The action types are publish and notification.

9. Path to the main site

The Mosquitto bridge mirrors both namespaces in both directions to the main site (topic outstation/# both 0, topic homeassistant/# both 0), so the main site sees the same Discovery and measurement topics as the dashboard. These are the defaults, the topic list is editable per connection in bridge.json.

Home Assistant on the main site creates its MQTT entities from the mirrored Discovery, the bridges’ devices as well as the dashboard’s own “Energy Node” device. The companion depends on this path (§14): it finds the dashboard’s series in Home Assistant only through these MQTT entities.

The dashboard manages the bridge but is not part of its data path:

flowchart LR
    UI["bridge UI"] -->|"PUT /api/v1/mqtt/bridge"| BJ[("bridge.json")]
    UI -->|"POST .../apply<br/>(mqtt_config + system_actions)"| REN["mqttbridge.Render"]
    BJ --> REN
    CRED[("mqtt_bridge_credentials.json")] --> REN
    REN --> STG["staged conf in the data directory"]
    STG --> HLP["root helper<br/>energy-node-dashboard-system-action"]
    HLP --> TGT["/etc/mosquitto/conf.d/bridge.conf"]
    HLP --> RST["restart mosquitto<br/>(rollback on error)"]
    TGT -.->|"drift comparison via checksum"| ST["GET /api/v1/mqtt/bridge/status"]
    SYS["$SYS connection state<br/>of the bridge client"] --> ST

The dashboard never writes to /etc itself. It puts a file into its own data directory and lets a root helper do the rest. The trust boundary is the helper, not the dashboard process: it reads exactly one compiled-in staging path, requires a regular file owned by the dashboard user of at most 8 KB and 200 lines, and accepts only lines that match a fixed directive allowlist (connection, address, topic and the like). A compromised dashboard process therefore cannot slip in log_dest, a new listener or any other directive. The helper keeps the previous file as a backup and restores it if Mosquitto does not restart. The status combines three sources: the systemd service state, the $SYS connection state and a checksum comparison between the stored and the installed configuration.

Written for Energy Node v0.11.0.

Edit on GitHub