Seed Sofar total sensors' high-water marks before the first poll (#180624)

This commit is contained in:
darkrain-nl
2026-08-30 12:12:43 +02:00
committed by GitHub
parent bd2d16fca5
commit ea7c98a060
2 changed files with 75 additions and 1 deletions
+36 -1
View File
@@ -7,13 +7,23 @@ from modbus_connection import ModbusError, ModbusTcpParams
from sofar_modbus.modern.device import SofarInverter, identify
from homeassistant.components.modbus import async_get_unit
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
SensorExtraStoredData,
SensorStateClass,
)
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
restore_state,
)
from .const import CONF_UNIT_ID, DOMAIN, SCAN_INTERVAL, SETTINGS_SCAN_INTERVAL
from .coordinator import SofarConfigEntry, SofarDataUpdateCoordinator, SofarRuntimeData
from .sensor import SENSOR_DESCRIPTIONS
_LOGGER = logging.getLogger(__name__)
@@ -34,6 +44,30 @@ async def _async_read_identity(entry: SofarConfigEntry, device: SofarInverter) -
return
def _async_seed_high_water_marks(
hass: HomeAssistant, serial: str, device: SofarInverter
) -> None:
"""Prime high-water marks before the first poll has nothing to compare."""
registry = er.async_get(hass)
last_states = restore_state.async_get(hass).last_states
for description in SENSOR_DESCRIPTIONS:
if description.state_class is not SensorStateClass.TOTAL_INCREASING:
continue
entity_id = registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, f"{serial}_{description.key}"
)
if entity_id is None or (stored := last_states.get(entity_id)) is None:
continue
if stored.extra_data is None:
continue
extra = SensorExtraStoredData.from_dict(stored.extra_data.as_dict())
if extra is None or not isinstance(extra.native_value, (int, float)):
continue
getattr(device, description.component).seed_high_water(
description.key, float(extra.native_value)
)
async def async_setup_entry(hass: HomeAssistant, entry: SofarConfigEntry) -> bool:
"""Set up Sofar Inverter Modbus from a config entry."""
serial = entry.unique_id
@@ -59,6 +93,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SofarConfigEntry) -> boo
model=model,
inverter_type=inverter_type,
)
_async_seed_high_water_marks(hass, serial, device)
readings = SofarDataUpdateCoordinator(
hass,
+39
View File
@@ -473,6 +473,45 @@ async def test_only_wired_battery_packs_become_devices(
assert entity_registry.async_get(total_id).device_id == inverter.id
async def test_total_survives_a_torn_first_poll_after_reload(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
mock_connection: MockModbusConnection,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test a reload's first poll is protected by the pre-reload total."""
mock_config_entry.add_to_hass(hass)
unit = mock_connection.for_unit(1)
unit.holding[0x068A] = 0
unit.holding[0x068B] = 10000 # load_consumption_total -> 1000.0 kWh
with patch(
"homeassistant.components.sofar.async_get_unit",
side_effect=lambda hass, entry, params, unit_id: mock_connection.for_unit(
unit_id
),
):
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
entity_id = entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, f"{MOCK_SERIAL}_load_consumption_total"
)
assert entity_id is not None
assert hass.states.get(entity_id).state == "1000.0"
await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
# A torn read on the reload's first poll, inside the 1% dip band.
unit.holding[0x068B] = 9995
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done(wait_background_tasks=True)
assert hass.states.get(entity_id).state == "1000.0"
async def test_battery_pack_appears_once_its_block_answers(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,