Reported by @maic-de in #464 (SUN2000-17K-MB0 + LUNA2000, v1.4.0). The code below is from release/v1.5.0, so this is not fixed in 1.5.0.
What happened
At 04:15 the LUNA2000 sat at its discharge cutoff. Shortly after, the integration published a battery power above 2,100,000 W, and by 07:11 a battery SOC above 6,500 %. The Marstek batteries in the same fleet then did not discharge for about two hours and the house imported from the grid instead.
The values are Huawei's "value not available" markers
The live block is decoded as:
# drivers/huawei.py
_BLOCK_LIVE = (37000, 5, "high", {
"inverter_state": (0, "u16", 1),
"battery_power": (1, "i32", 1),
"battery_voltage": (3, "u16", 0.1),
"battery_soc": (4, "u16", 0.1),
})
battery_soc is u16 × 0.1. Huawei's invalid marker for u16 is 0xFFFF = 65535 → 6553.5 %, which matches the reported "more than 6,500 %" exactly.
battery_power is i32, where the marker is 0x7FFFFFFF = 2,147,483,647. Consistent with the reported magnitude, though not derivable to the digit from the screenshot.
Why they reach the control layer
The decode loop discards a block that fails to read, which is correct:
# drivers/huawei.py
regs = await self._client.async_read_holding_block(start, count)
if regs is None:
# A failed block omits its keys rather than publishing zeros; the
# coordinator treats missing keys as stale, which is correct.
continue
But a block that reads successfully and contains a marker is decoded and published verbatim. Downstream, the only plausibility guard in infra/coordinator.py is is_untrusted_energy_reading, gated on state_class == "total_increasing". battery_soc and battery_power are measurement, so nothing inspects them. There is no upper bound on SOC between the register and the allocator.
Consequence
A SOC of 6553 % places the battery far above every floor, cutoff and reserve, and a battery power of 2.1 GW corrupts the derived house-consumption balance (battery + grid + solar). The fleet allocator then has no usable picture, which is what left the Marstek batteries idle.
So this is not cosmetic: an invalid reading on one driver suspends control of the whole fleet.
The guard already exists, on the other path
control/pack_soc.py filters per-pack SOC on the Marstek side:
and isinstance(value, (int, float))
and 0 <= value <= 100
with the comment "Values are bounded on read: … an out-of-range reading here would move a charge or discharge limit." That is precisely the failure above, one driver over.
Suggested direction
Treat a sentinel like a failed read — omit the key — so the existing "missing keys are stale" behaviour applies and no new policy is needed:
- reject
0xFFFF / 0xFFFFFFFF for unsigned and 0x7FFF / 0x7FFFFFFF for signed reads before scaling;
- additionally bound the physical ranges (
battery_soc 0–100 %, power against the inverter's rated maximum), since a marker is not the only way a nonsense value can arrive;
- log at WARNING when a value is dropped, so the condition is visible without debug logging.
Worth considering whether the bound belongs in the coordinator merge path rather than per driver, so every brand gets it.
Open question (does not block a fix)
The likely trigger is the battery being asleep at its cutoff overnight, with the inverter answering the battery registers with markers while it sleeps. That is a hypothesis. It would determine whether dropping the value is sufficient or whether the driver should also report the battery as temporarily unavailable.
Reported by @maic-de in #464 (SUN2000-17K-MB0 + LUNA2000, v1.4.0). The code below is from
release/v1.5.0, so this is not fixed in 1.5.0.What happened
At 04:15 the LUNA2000 sat at its discharge cutoff. Shortly after, the integration published a battery power above 2,100,000 W, and by 07:11 a battery SOC above 6,500 %. The Marstek batteries in the same fleet then did not discharge for about two hours and the house imported from the grid instead.
The values are Huawei's "value not available" markers
The live block is decoded as:
battery_socis u16 × 0.1. Huawei's invalid marker for u16 is0xFFFF= 65535 → 6553.5 %, which matches the reported "more than 6,500 %" exactly.battery_poweris i32, where the marker is0x7FFFFFFF= 2,147,483,647. Consistent with the reported magnitude, though not derivable to the digit from the screenshot.Why they reach the control layer
The decode loop discards a block that fails to read, which is correct:
But a block that reads successfully and contains a marker is decoded and published verbatim. Downstream, the only plausibility guard in
infra/coordinator.pyisis_untrusted_energy_reading, gated onstate_class == "total_increasing".battery_socandbattery_poweraremeasurement, so nothing inspects them. There is no upper bound on SOC between the register and the allocator.Consequence
A SOC of 6553 % places the battery far above every floor, cutoff and reserve, and a battery power of 2.1 GW corrupts the derived house-consumption balance (battery + grid + solar). The fleet allocator then has no usable picture, which is what left the Marstek batteries idle.
So this is not cosmetic: an invalid reading on one driver suspends control of the whole fleet.
The guard already exists, on the other path
control/pack_soc.pyfilters per-pack SOC on the Marstek side:with the comment "Values are bounded on read: … an out-of-range reading here would move a charge or discharge limit." That is precisely the failure above, one driver over.
Suggested direction
Treat a sentinel like a failed read — omit the key — so the existing "missing keys are stale" behaviour applies and no new policy is needed:
0xFFFF/0xFFFFFFFFfor unsigned and0x7FFF/0x7FFFFFFFfor signed reads before scaling;battery_soc0–100 %, power against the inverter's rated maximum), since a marker is not the only way a nonsense value can arrive;Worth considering whether the bound belongs in the coordinator merge path rather than per driver, so every brand gets it.
Open question (does not block a fix)
The likely trigger is the battery being asleep at its cutoff overnight, with the inverter answering the battery registers with markers while it sleeps. That is a hypothesis. It would determine whether dropping the value is sufficient or whether the driver should also report the battery as temporarily unavailable.