Skip to content

fix(gateway): keep energy history across inverter changes - #5119

Open
mgazza wants to merge 4 commits into
mainfrom
fix/gateway-site-energy-entities
Open

mgazza wants to merge 4 commits into
mainfrom
fix/gateway-site-energy-entities

Conversation

@mgazza

@mgazza mgazza commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

Problem

automatic_config() bound load_today, pv_today, import_today and export_today to the first bound inverter's serial-named entities (sensor.predbat_gateway_<suffix>_load_today, ...). Load and PV history are fetched by entity id, so any change of the bound serial started that history from zero. That covers an inverter replacement, or a re-discovery that picks a different control target such as a second AIO moving control to the Gateway. The load forecast, PV calibration and ML load model then ran with no days of history.

Fix

  1. Site-level counters. Publish sensor.predbat_gateway_pv_today, _import_today, _export_today and _load_today, which carry no serial, and bind the four args to them.
    • automatic_config() records the slot 0 inverter, the one the energy args always used, as the source, so the plan sees the same data.
    • The counters are published on each telemetry that includes the source inverter, and once during auto-config so they exist before PredBat's first fetch. While the source is missing from a status they keep their last value, as the per-inverter entities do.
    • The per-inverter *_today entities are still published.
  2. No history lost on upgrade. A new generic hook, PredBat.set_history_alias(entity_id, legacy_entity_ids), is exposed to components through ComponentBase and is a no-op in MockBase.
    • get_history_wrapper fills an aliased entity's history in with the legacy entities' records from before its own first record. Every history reader goes through it (fetch, PV calibration, ML load, charts), so all of them see one continuous series.
    • Where both entities were recorded, the new one wins.
    • Legacy entities are read untracked, so the history cache doesn't keep refreshing them. Once the entity's own history covers the requested window they aren't read at all.
    • With no history anywhere the result is unchanged (None, or raising when required).
    • The gateway registers each site-level entity's serial-named predecessor as its alias.
  3. Continuous when the source changes. Without this, a mid-day switch of source joined two counters into one series; PredBat's history reads a rise as energy used and a small drop as a reset.
    • The published value now carries on from the last value published that day, offset by the difference at the switch.
    • The offset is dropped when the source counter resets for a new day; a switch across midnight starts from the new counter.
    • The source serial, its raw reading and the offset are kept in the entity's attributes, so a restart carries on the same way.

No other gateway entity uses the new names: the site-level entities are ems_total_*, sub<n>_*, online and the ev_* charger entities, and a test checks each new id is written exactly once per status.

Verification

The reviewer's cut-over simulation, run through PredBat's real fetch code: the old entity has 8 days of history and the new one 2 hours.

load history age PV booked [-48h,-24h] load / PV today so far
serial-named entities (before) 8 days 11.96 kWh 7.00 / 7.00 kWh
renamed, no alias 0 days 0.00 kWh 1.00 / 1.00 kWh
renamed, with alias (this PR) 8 days 11.96 kWh 7.00 / 7.00 kWh

A switch of source mid-day, run through utils.minute_data (true total 12.00 kWh). The "day without a switch" column is the same site sampled the same way:

case without offset this PR same day without a switch
+0.8 kWh step, 1 min samples 12.79 kWh 11.99 kWh 11.99 kWh
-0.5 kWh step, 1 min samples 11.49 kWh 11.99 kWh 11.99 kWh
+3.0 kWh step, 5 min samples 14.96 kWh 11.96 kWh 11.96 kWh

Limits

  • Switching to the gateway from a different integration still starts these histories from zero. The new ids are gateway-specific, and only the gateway's own serial-named entities are registered as aliases.
  • If the old source is missing from the status where the switch happens (for example the inverter was removed), the energy used between its last status and the switch is not counted: at most one status interval.

Tests

  • tests/test_history_alias.py (new, registered as history_alias):
    • prepend_older_history edge cases.
    • get_history_wrapper: no alias; new entity with no history yet; partial history; a fully covered window that skips the legacy read; no data anywhere still fails; untracked legacy reads through the cache; removing aliases.
    • The cut-over simulation above.
  • test_gateway.py:
    • Site counters mirror the source, with none published before a source is chosen.
    • The source is chosen by serial, not list order, and a missing source publishes nothing.
    • A source with no energy block publishes 0.
    • No id collisions.
    • On a Gateway + two AIOs site, the bound energy entities are written with the Gateway's counters.
    • The energy args stay on the site ids across an inverter replacement, and with several inverters the source is slot 0.
    • The site counters are published during auto-config, and the history aliases are registered.
    • The reviewer's three switch cases book the same as a day without a switch, and the offset ends at the midnight reset.
    • The offset survives a restart: same source, a different source on the same day, a different source the next day, and nothing stored.
  • test_mock_base.py: the delegate contract now covers set_history_alias.

I reverted each piece individually (alias lookup, coverage skip, untracked legacy read, self-alias guard, the offset, the reset clear, restoring after a restart, the same-day check on a switch), and each revert makes a test fail. ./run_all --test gateway passes (288), --test history_alias and --test mock_base pass, and ./run_all --quick passes.

🤖 Generated with Claude Code

mgazza and others added 4 commits September 16, 2026 17:54
automatic_config() pointed load_today, pv_today, import_today and
export_today at the first bound inverter's serial-named entities
(sensor.predbat_gateway_<suffix>_load_today, ...). Load and PV history are
fetched by entity id, so any change of the bound serial - an inverter
replacement, or a re-discovery that picks a different control target -
started history from zero and left the load forecast and PV calibration
with no days to work from.

Publish site-level counters without a serial -
sensor.predbat_gateway_{pv,import,export,load}_today - and bind the four
args to them. automatic_config() records the slot 0 inverter as the
source, so the values are the ones the plan already used. They are
published on each telemetry that includes the source inverter, and once
during auto-config so they exist before PredBat's first fetch. The
per-inverter counters are still published, from the same helper.

Existing gateway installs start these four histories again once under the
new entity ids.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
History is fetched by entity id, so pointing an arg such as load_today or
pv_today at a new entity id restarts its history from zero, and the load
forecast, PV calibration and ML load model then work from no days at all.

Add PredBat.set_history_alias(entity_id, legacy_entity_ids), exposed to
components through ComponentBase. get_history_wrapper then fills in the
legacy entities' records from before the entity's own first record, so
every history reader (fetch, PV calibration, ML load, charts) sees one
continuous series. Where both were recorded the new entity wins. Legacy
entities are read untracked so the history cache does not keep refreshing
them, and are not read at all once the entity's own history covers the
requested window. With no history anywhere the result is unchanged (None,
raising when required).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moving load_today, pv_today, import_today and export_today onto the
site-level entities left every existing gateway site with no load, PV,
import or export history under the new ids. automatic_config() now
registers each site-level entity's serial-named predecessor
(sensor.predbat_gateway_<suffix>_*_today, which is still published) as its
history alias, so the history read before the site entity existed comes
from there and the rename loses nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e changes

When the inverter feeding the site-level *_today counters changed mid-day,
the series jumped from the old counter's reading to the new one's. PredBat's
history reads a rise as energy used and a small drop as a reset, so a
0.8 kWh step booked 12.79 kWh for a 12.00 kWh day and a -0.5 kWh step
booked 11.49 kWh.

The published value now carries on from the last value published that day,
offset by the difference at the switch, and the offset is dropped when the
source counter resets for a new day (a switch across midnight starts from
the new counter). The source serial, its raw reading and the offset are
kept in the entity's attributes, so a restart carries on the same way
rather than stepping. With the offset the switch day books the same as a
day without a switch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant