Power Management (Generic Energy Dashboard)
GEM talks to many power devices — battery gateways (Tesla Powerwall, Enphase Encharge, SPAN), solar inverters (Enphase, SolarEdge, Fronius, Sol-Ark), whole-home and circuit meters (Emporia Vue, Sense, Meross EM), UPS/PDUs (CyberPower, WattBox, Panamax) and EV chargers (OpenEVSE, Tesla Wall Connector). Each driver reports power data under its own attribute names and units. To make a single dashboard work across all of them without a per-site build, GEM normalizes that vocabulary in the attribute registry and exposes it through the Power Flow widget.
The Power Flow widget
Add it to any UI page like any other widget (UIs → Pages → add widget → Power Flow). It renders a live energy-flow diagram: a central Home node with Solar, Grid and Battery around it. Flow lines animate in the direction power is moving and speed up under load; the battery node fills from the bottom to show state of charge and shows a bolt overlay while it is charging. Nodes a device doesn't report (e.g. a solar-only inverter with no battery) are simply omitted. In aggregate mode the battery fill uses the same energy-weighted fleet SOC as the Battery Backup widget (see below), so the two never disagree.
Settings:
| Setting | Purpose |
|---|---|
| Power Device | The inverter / gateway / battery to visualize (e.g. a Tesla Powerwall device). |
| Aggregate All Power Devices | Ignore the device above and sum every power-reporting device on the site into one flow. |
| Refresh (ms) | Poll interval for the flow snapshot (default 5000). |
| Show Battery SOC | Render battery state of charge as the battery fill level. |
The Top Circuits widget
Where Power Flow answers "solar vs grid vs battery," Top Circuits answers "which loads."
It renders a live, ranked bar list of the highest-draw circuits — the per-circuit complement to
the flow diagram. It reads every zone tagged with the power.circuit role (SPAN panel breakers
today; any sub-meter that tags the role appears automatically), normalizes to watts and sorts by
live draw, biggest consumers first.
Settings:
| Setting | Purpose |
|---|---|
| Panel Device | Limit to circuits of a single panel / sub-metering device. Leave empty to include every circuit on the site. |
| Circuits Shown | How many top-draw circuits to list (default 8). |
| Refresh (ms) | Poll interval for the circuit snapshot (default 5000). |
| Show Total | Show the summed draw of all circuits (before the limit) in the header. |
Circuit values are signed: positive is consumption, negative is backfeed/export (a solar or
battery tab, or a circuit whose CT clamp is physically reversed — see the SPAN panel docs
and the zone ct_clamp_reversed attribute). Exporting circuits render in green.
More power widgets
All ride the same metric registry — no per-device code, no extra setup beyond tagging the driver.
| Widget | Shows | Reads |
|---|---|---|
| Self-Sufficiency | Ring gauge: share of current load met locally (solar + battery) vs the grid, (load − max(0, grid_import)) / load. | power.load, power.grid |
| Battery Backup | SOC ring, energy remaining (kWh), and estimated backup runtime at the current load (energy_remaining ÷ load). Charge/discharge status; flips to "On Backup" when the grid is down. Aggregate mode uses the energy-weighted fleet SOC computed server-side (shared with Power Flow). | battery.soc, battery.energy_remaining, power.load, power.battery, power.grid_status |
| Energy Today | Today's consumed / produced / net energy, accumulated since local midnight. Survives restarts. | energy.consumed, energy.produced |
How "today" works. energy.* roles are cumulative monotonic counters, so "today" is current − the counter's value at the start of the local day. The server snapshots every counter into one
bounded system attribute (metric_energy_today_baseline) once per day — a UPSERT, not a
time-series — and the read path differences against it. A daily rollover task (every 5 min,
GemServer.startMetricEnergyRollup) captures the midnight boundary; the baseline persists, so a
mid-day restart preserves the day's totals. A counter reset/replacement is clamped to ≥ 0 and
self-heals at the next rollover. Day boundary is server-local time (GEM sets the OS clock to the
site timezone at boot).
The Water Flow widget
The registry mechanism is not power-specific (see How it works below). Water Flow is the irrigation counterpart to Power Flow: add it to any UI page (UIs → Pages → add widget → Water Flow) and it renders a live diagram of an irrigation controller — a central supply node showing the flow rate (L/min) fanning out to the controller's zones. Lines animate toward whichever zones are watering, and the supply rate animates faster as flow rises; each zone node shows its sprinkler state plus either its run time-remaining (while watering) or its week-to-date volume (while idle). Point it at a single controller (e.g. a Hunter Hydrawise) or aggregate every water device on the site.
Like the power widgets it is generic over vendors — any driver that tags the water.*
metric roles lights up the same widget with no per-driver code. A device counts as a
"water device" when its driver tags water.flowrate (the controller's flow meter) or its
zones tag water.flowrate / water.usage. When no device-level meter is present the
supply rate falls back to the sum of per-zone flow.
Settings:
| Setting | Purpose |
|---|---|
| Water Controller | The irrigation controller device to visualize. Leave empty and enable Aggregate to combine all water devices. |
| Aggregate All Water Devices | Ignore the device above and combine every water-reporting controller on the site into one flow. |
| Units | Display flow and volume in metric (litres) or imperial (gallons). Canonical values are litres; conversion is display-only. |
| Refresh (ms) | Poll interval for the water-flow snapshot (default 5000). |
How it works — canonical metric roles
The role vocabulary and unit normalization live in lib/attribute_metrics.js (the single
source of truth for the catalog); resolution against live device/zone values lives in
lib/metric_resolver.js. Roles are domain-namespaced <domain>.<concept> (power.solar,
battery.soc, water.flowrate, water.usage, …) and the mechanism is deliberately
domain-agnostic — not power-specific. Irrigation is the worked non-power example: a Hydrawise
controller's flow meter tags water.flowrate (flow, L/min) and per-zone week-to-date volume
tags water.usage (counter, litres) under a dedicated volume dimension.
Each role declares a dimension (with a canonical unit and affine value × scale + offset
conversions, so e.g. temperature in °F converts correctly) and a kind: flow (instantaneous
rate, signed), counter (cumulative total, windowed by differencing), level (bounded gauge),
or status (enum). A dashboard can render a role it has never seen from its kind alone.
The attribute registry tags each power attribute with a metric_role and a source
unit. A generic consumer reads the role, not the driver-specific name — so production_now_w
(Enphase), pv_power (SolarEdge), pv_power_w (Fronius) and solar_power_w (Tesla) all resolve
to the same power.solar concept. Driver attribute names are never renamed; normalization lives
entirely in the registry plus the server resolver.
Instantaneous flow roles are normalized to watts with one fixed sign convention:
| Role | Meaning | Sign convention |
|---|---|---|
power.solar | Solar/PV generation | ≥ 0 |
power.load | Site/home consumption | ≥ 0 |
power.grid | Grid power | + import, − export |
power.battery | Battery power | + discharge, − charge |
power.generator | Generator output | ≥ 0 |
power.circuit | Per-circuit / sub-meter draw (zone-level, one per breaker) | + consume, − backfeed |
Cumulative counter roles (watt-hours): energy.consumed, energy.produced,
energy.grid_import, energy.grid_export. Storage/status roles: battery.soc (%),
battery.energy_remaining / battery.capacity (kWh), power.grid_status, power.mode.
Roles can be tagged on devices (whole-gateway flow) or zones (per-circuit draw); the resolver keys by the entity's driver either way.
When a driver's native attribute uses the opposite sign for its role, its seed row carries
metric_sign: 'inverted' and the resolver negates it (e.g. Enphase net_power_w, which the
driver reports as positive-for-export). When a driver's load attribute is missing or its sign is
unreliable (e.g. Fronius reports consumption as negative), the resolver derives load from the
energy balance: load = solar + grid + battery.
Drivers currently tagged
| Driver | Solar | Grid | Battery | Load | SOC | Notes |
|---|---|---|---|---|---|---|
| Tesla Powerwall | ✓ | ✓ | ✓ | ✓ | ✓ | Verified against live firmware 26.10.3. Lifetime energy counters also tagged (solar→energy.produced, load→energy.consumed, grid→energy.grid_import/energy.grid_export) → Energy Today. |
| Enphase Envoy/IQ | ✓ | ✓ | ✓ | ✓ | ✓ | Grid sign inverted in registry; battery sign should be field-verified. |
| SolarEdge | ✓ | ✓ | ✓ | ✓ | ✓ | Requires the power-flow API (consumption/storage meter). Values in kW. |
| Fronius | ✓ | ✓ | ✓ | derived | — | Load derived (Fronius P_Load sign convention). |
| SPAN | — | — | — | — | ✓ | Device-level SOC; per-circuit power (power.circuit) and energy counters (energy.consumed/energy.produced) tagged at the zone level → Top Circuits. |
| Sense | ✓ | — | — | ✓ | — | Whole-home load and solar (when a solar signal is configured) at the device level → Power Flow; per-detected-device/circuit draw (power.circuit) tagged at the zone level → Top Circuits. |
| Cordex HP | — | — | ✓ | ✓ | — | DC power plant. Load and battery power derived from bus voltage × current (the plant reports volts + amps, not watts). Battery sign follows the Cordex battery_current convention — verify against a discharge (AC-fail) event and set metric_sign: inverted if reversed. |
| Emporia Vue | — | — | — | — | — | Per-circuit draw (power.circuit) tagged at the zone level → Top Circuits. |
| Meross EM | — | — | — | — | — | Per-channel draw (power.circuit) tagged at the zone level → Top Circuits. |
Sub-meter / metered drivers expose per-circuit draw via the power.circuit zone role → Top
Circuits; SPAN, Emporia Vue and Meross EM are tagged. Sense additionally reports whole-home load
(and solar, when a solar signal is configured) at the device level → Power Flow. UPS/PDUs are not
yet tagged for the flow diagram.
Adding a new driver to the dashboard
No widget or server code is needed — tag the driver's attributes in
lib/attribute_registry_seed.js:
{name: 'pv_power_w', system_target: 'device', value_type: 'float', category: 'state',
context: 'my_inverter', history: true, unit: 'W',
description: 'PV production.', metric_role: 'power.solar'},
Pick the attribute whose native sign already matches the convention above; if none does, add
metric_sign: 'inverted'. Units are W (default), kW or MW. On the next boot the seeder
applies the tags and the Power Flow widget picks the device up automatically.
API — get_metric_energy_flow
Socket event (and GemApp.getMetricEnergyFlow(params) on the client):
params:{device_id}for a single device, or omit for a site-wide aggregate.- Returns
{devices: [...], totals: {...}}where each device is{device_id, device_name, driver, solar_w, grid_w, battery_w, load_w, battery_soc, grid_status, mode}with flows in watts following the sign convention above. totalssums the per-device flows (solar_w,grid_w,battery_w,load_w,battery_energy_remaining_wh,battery_capacity_wh) and adds a single fleetbattery_soc(%). It is energy-weighted — total stored ÷ total capacity — so batteries of different size or charge combine correctly rather than a plain mean of per-device percentages over-weighting a small full pack. For batteries that report a percentage but no capacity (e.g. SolarEdge, SPAN) it falls back to a simple mean of those devices' SOC;nullwhen no battery reports SOC. Both the Power Flow and Battery Backup widgets read this one number so their aggregate SOC always agrees.
API — get_metric_circuit_power
Socket event (and GemApp.getMetricCircuitPower(params) on the client):
params:{device_id?, limit?}— optionally restrict to one panel device and/or cap the number of returned circuits.- Returns
{circuits: [...], total_w}where each circuit is{zone_id, zone_name, label, device_id, device_name, watts}, sorted bywattsdescending (signed: biggest consumers first, exporters last).total_wsums all circuits before anylimitis applied.
These resolvers read live values straight from the in-memory device/zone objects, so they reflect the latest poll without a database round-trip.
API — get_metric_energy_today
Socket event (and GemApp.getMetricEnergyToday() on the client):
- Returns
{consumed_wh, produced_wh, net_wh, by_role, date, baseline_ready, counter_count}— today's energy summed across every entity carrying anenergy.*counter role, differenced against the persisted daily baseline.baseline_readyis false only in the brief window before the first rollover of the day. Read-only; the rollover task owns the baseline write.
The homeowner voice assistant answers daily-total questions from the same resolver (the
energy_today skill): "how much energy did I use today?", "how much solar did I make today?",
"am I net positive today?" — the cumulative-kWh sibling of the instantaneous power_flow and
state-of-charge battery_status skills.
API — get_metric_water_flow
Socket event (and GemApp.getMetricWaterFlow(params) on the client) — the water counterpart to
get_metric_energy_flow:
params:{device_id}for a single controller, or omit for a site-wide aggregate of every water device.- Returns
{devices: [...], zones: [...], totals: {...}}. Each device is{device_id, device_name, driver, flowrate_lpm, zone_count, active_count, total_usage_l, zones: [...]}, and each zone is{zone_id, label, state, watering, usage_l, flowrate_lpm, time_remaining}. The top-levelzonesflattens every device's zones, sorted active-first then by usage, so a capped widget surfaces the zones that are actually watering.totalssumsflowrate_lpm,zone_count,active_countandtotal_usage_l. - Flow is canonical litres/min and volume canonical litres; the widget converts to gallons for imperial display. Reads live values straight from the in-memory device/zone objects.