Skip to content

fix(gateway): write entities for the bound control target, not just primary units - #4360

Merged
springfall2008 merged 1 commit into
mainfrom
fix/gwmqtt-entity-binding-divergence
Jul 29, 2026
Merged

fix(gateway): write entities for the bound control target, not just primary units#4360
springfall2008 merged 1 commit into
mainfrom
fix/gwmqtt-entity-binding-divergence

Conversation

@mgazza

@mgazza mgazza commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes #4356.

The bug

On a multi-AIO site automatic_config() picks the Gateway/EMS as the control target (GivTCP rules) and binds every PredBat arg to its serial suffix:

elif gateway_units and len(aios) > 1:
    inverters = gateway_units[:1]     # Gateway is the control point
...
suffix0 = inverters[0].serial[-6:].lower()
self.set_arg("inverter_time", [f"sensor.{self.prefix}_gateway_{suffix0}_inverter_time"])

But _inject_entities() only ever wrote entities for units flagged primary — and firmware never flags a Gateway or EMS primary (it is set on battery inverters). So on those sites none of the bound entities were ever written. They kept whatever value they last held and silently froze.

Impact

Seen in production: a site froze for 69 minutes with byte-identical readings every cycle —

SoC: 20.52kW 76%, battery power 5467W, grid power 5336W, load power 348W, PV Power 217W

— while the battery was really force-exporting at ~5.5 kW. PredBat kept planning and issuing control against a stale 76% SoC.

The only symptom that escaped was the clock-skew warning, because inverter_time is the one bound value whose staleness is self-evident against wall clock. It grew linearly (−37 → −42 → … → −98 min) and told the user to "fix your inverter time" — misleading, since the inverter clock was fine. Everything else failed silently, because nothing about a stale SoC looks wrong on its own.

Verified against a live device: the gateway was publishing healthy frames throughout (current timestamp, all inverters present) and PredBat's broker session was receiving them. Nothing was wrong with the device or the transport — only the entity binding.

There is a second divergence mode with the same cause: _needs_reconfigure() only re-runs on a new serial or new EV charger, so if the primary flag moves between already-known units the binding goes stale the same way. This PR fixes the write side, which covers both.

The fix

  • Inject a unit when it is primary or when automatic_config bound args to it. _suffix_to_serial is already populated by automatic_config with exactly the control-target suffixes, so it is the authoritative record of what PredBat reads.
  • Write inverter_time under the bound suffix rather than the primary's.
  • Non-primary units that are not the control target stay skipped, preserving the existing guard against EMS/gateway units double-counting power readings on the dashboard.

Tests

New TestBoundEntitiesAreWritten asserts the invariant directly — every entity automatic_config binds must actually be written by _inject_entities — using a Gateway + 2 AIO topology matching the field case.

Confirmed RED first. Before the fix it reported:

automatic_config bound inverter_time to sensor.predbat_gateway_15g357_inverter_time
but _inject_entities never wrote it; written entities were: [... only _35g421 and _32g070 ...]

After: all 251 gateway tests pass, and unit_test.py --quick is green. flake8 output is unchanged from the main baseline (the remaining C901/E402 are pre-existing); interrogate stays at 100%.

…rimary units

On a multi-AIO site automatic_config picks the Gateway/EMS as the control target
and binds every PredBat arg to its serial suffix. _inject_entities only ever
wrote entities for units flagged primary — and firmware never flags a Gateway or
EMS primary — so none of the bound entities were written. They held whatever
value they had and silently froze.

Observed in production: a site froze for 69 minutes with byte-identical SoC,
battery power, grid power, load power and PV power, while PredBat continued to
plan and issue control against a stale 76% SoC. The only escaping symptom was the
clock-skew warning, because inverter_time is the one bound value whose staleness
is self-evident against wall clock — which sends users to fix an inverter clock
that is not broken.

Inject a unit when it is primary OR when automatic_config bound args to it
(tracked in _suffix_to_serial), and write inverter_time under the bound suffix
rather than the primary's. Non-primary units that are not the control target stay
skipped, preserving the existing guard against double-counted power readings.

Adds TestBoundEntitiesAreWritten, which asserts the invariant directly: every
entity automatic_config binds must actually be written by _inject_entities.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread apps/predbat/gateway.py
continue
suffix = inv.serial[-6:].lower() if len(inv.serial) > 6 else inv.serial.lower()
self._inject_inverter_entities(inv, suffix)
self._inject_inverter_entities(inv, _serial_suffix(inv.serial))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just publish them all?

@springfall2008
springfall2008 merged commit 07d54d5 into main Jul 29, 2026
2 checks passed
@springfall2008
springfall2008 deleted the fix/gwmqtt-entity-binding-divergence branch July 29, 2026 13:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants