diff --git a/homeassistant/components/sofar/__init__.py b/homeassistant/components/sofar/__init__.py index 6499636d5621..3d3ae5dc2f0f 100644 --- a/homeassistant/components/sofar/__init__.py +++ b/homeassistant/components/sofar/__init__.py @@ -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, diff --git a/tests/components/sofar/test_init.py b/tests/components/sofar/test_init.py index 098806cf82cb..2c1f9340fc90 100644 --- a/tests/components/sofar/test_init.py +++ b/tests/components/sofar/test_init.py @@ -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,