Skip to content

Isolate per-entity failures during number platform setup (#450) - #458

Open
coogle wants to merge 1 commit into
sfstar:mainfrom
coogle:fix/number-platform-fault-isolation
Open

Isolate per-entity failures during number platform setup (#450)#458
coogle wants to merge 1 commit into
sfstar:mainfrom
coogle:fix/number-platform-fault-isolation

Conversation

@coogle

@coogle coogle commented Aug 21, 2026

Copy link
Copy Markdown

Problem

VictronNumber.__init__ evaluates value_fn eagerly at construction time:

self._attr_native_value = self.description.value_fn(
    self.coordinator.processed_data(),
    self.description.slave,
    self.description.key,
)

If a unit in the stored register set returns no data for that register, the default value_fn in base.py raises KeyError. There is no try/except in number.py, so the exception propagates out of async_setup_entry and every number entity across every unit fails to be created — not only the one belonging to the device that is actually missing.

The failure is silent (nothing logged above debug), and the blast radius is unrelated to the missing device.

Real-world impact

An unpowered SmartShunt (unit 239) stopped populating battery_power. That removed number.victron_vebus_activein_currentlimit_237 — the MultiPlus AC input current limit — leaving the inverter pinned to a stale 50 A limit on a 30 A supply for a month. Nothing in the log explained it; the control simply read unavailable.

Sensors from the same integration, polling the same dead unit, were unaffected: 629 sensors available alongside 0 numbers.

#450 reports the same defect via evcharger_maxcurrent on slave 41.

Why sensors are immune and numbers are not

sensor.py evaluates value_fn lazily inside _handle_coordinator_update, wrapped in try/except, and falls back to None. number.py evaluates at construction and has no guard. That asymmetry is the bug.

Change

Two changes, both scoped to number.py:

  • async_setup_entry skips an individual entity whose register has no data, logging a warning naming the entity and unit, instead of aborting the platform.
  • native_value returns None if the key disappears after setup (device powered off later) rather than raising on every coordinator update.

No behaviour change when all configured units are present and reporting.

Note on the obvious alternative

Changing base.py's default value_fn to dict.get() looks like the smaller fix, but it only relocates the failure: native_value does value > round(UINT16_MAX / 2), which raises TypeError on None. Keeping the handling in number.py confines the change to the platform that lacks the guard.

Testing

Verified on a live install (HA 2025.12, Venus GX, MultiPlus-II 12/3000, MPPT, 2× BLE BMS, one dead unit).

Before: 31 number entities, 0 available. After: all numbers available except the single skipped one, with one log line:

WARNING (MainThread) [custom_components.victron.number] Skipping number entity
'battery_power' for unit 0: the unit returned no data for this register.
Other entities are unaffected

A unit listed in the stored register set that returns no data for a given
register raises KeyError inside VictronNumber.__init__, which evaluates
value_fn eagerly at construction time. That exception propagates out of
async_setup_entry, so a single dead unit removes EVERY number entity across
ALL units.

The failure is silent and the blast radius is unrelated to the dead device.
Observed in the wild: an unpowered SmartShunt (unit 239) not populating
battery_power took down number.victron_vebus_activein_currentlimit_237 -- the
MultiPlus AC input current limit -- leaving the inverter pinned to a stale
50A limit on a 30A supply for a month with nothing logged above debug.
Issue sfstar#450 reports the same defect via evcharger_maxcurrent on slave 41.

The sensor platform already tolerates this: it evaluates value_fn lazily
inside _handle_coordinator_update and falls back to None. Only numbers
evaluate at construction, and only numbers lack a guard -- which is why a
dead unit leaves hundreds of working sensors alongside zero working numbers.

Two changes, both scoped to number.py:

- async_setup_entry skips an individual entity whose register has no data and
  logs a warning naming the entity and unit, instead of aborting the platform.
- native_value returns None if the key disappears after setup (device powered
  off later) rather than raising on every coordinator update.

No behaviour change when all configured units are present and reporting.
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