Skip to main content

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:

SettingPurpose
Power DeviceThe inverter / gateway / battery to visualize (e.g. a Tesla Powerwall device).
Aggregate All Power DevicesIgnore 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 SOCRender 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:

SettingPurpose
Panel DeviceLimit to circuits of a single panel / sub-metering device. Leave empty to include every circuit on the site.
Circuits ShownHow many top-draw circuits to list (default 8).
Refresh (ms)Poll interval for the circuit snapshot (default 5000).
Show TotalShow 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.

WidgetShowsReads
Self-SufficiencyRing gauge: share of current load met locally (solar + battery) vs the grid, (load − max(0, grid_import)) / load.power.load, power.grid
Battery BackupSOC 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 TodayToday'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:

SettingPurpose
Water ControllerThe irrigation controller device to visualize. Leave empty and enable Aggregate to combine all water devices.
Aggregate All Water DevicesIgnore the device above and combine every water-reporting controller on the site into one flow.
UnitsDisplay 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:

RoleMeaningSign convention
power.solarSolar/PV generation≥ 0
power.loadSite/home consumption≥ 0
power.gridGrid power+ import, − export
power.batteryBattery power+ discharge, − charge
power.generatorGenerator output≥ 0
power.circuitPer-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

DriverSolarGridBatteryLoadSOCNotes
Tesla PowerwallVerified 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/IQGrid sign inverted in registry; battery sign should be field-verified.
SolarEdgeRequires the power-flow API (consumption/storage meter). Values in kW.
FroniusderivedLoad derived (Fronius P_Load sign convention).
SPANDevice-level SOC; per-circuit power (power.circuit) and energy counters (energy.consumed/energy.produced) tagged at the zone level → Top Circuits.
SenseWhole-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 HPDC 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 VuePer-circuit draw (power.circuit) tagged at the zone level → Top Circuits.
Meross EMPer-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.
  • totals sums the per-device flows (solar_w, grid_w, battery_w, load_w, battery_energy_remaining_wh, battery_capacity_wh) and adds a single fleet battery_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; null when 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 by watts descending (signed: biggest consumers first, exporters last). total_w sums all circuits before any limit is 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 an energy.* counter role, differenced against the persisted daily baseline. baseline_ready is 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-level zones flattens every device's zones, sorted active-first then by usage, so a capped widget surfaces the zones that are actually watering. totals sums flowrate_lpm, zone_count, active_count and total_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.