diff --git a/homeassistant/components/weatherflow/sensor.py b/homeassistant/components/weatherflow/sensor.py index 32220c961add..519604814898 100644 --- a/homeassistant/components/weatherflow/sensor.py +++ b/homeassistant/components/weatherflow/sensor.py @@ -16,8 +16,8 @@ from pyweatherflowudp.device import ( ) from homeassistant.components.sensor import ( + RestoreSensor, SensorDeviceClass, - SensorEntity, SensorEntityDescription, SensorStateClass, ) @@ -61,13 +61,18 @@ class WeatherFlowSensorEntityDescription(SensorEntityDescription): raw_data_conv_fn: Callable[[Any], datetime | StateType] device_attr: str | None = None + restore_last_value: bool = False # No value means there is nothing to report, not that the device is unavailable. none_is_unknown: bool = False event_subscriptions: list[str] = field(default_factory=lambda: [EVENT_OBSERVATION]) + def get_raw_value(self, device: WeatherFlowDevice) -> Any: + """Return the sensor value as reported by the device.""" + return getattr(device, self.device_attr or self.key) + def get_native_value(self, device: WeatherFlowDevice) -> datetime | StateType: """Return the parsed sensor value.""" - if (raw_sensor_data := getattr(device, self.device_attr or self.key)) is None: + if (raw_sensor_data := self.get_raw_value(device)) is None: return None return self.raw_data_conv_fn(raw_sensor_data) @@ -161,6 +166,7 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="lightning_strike_last_distance", device_attr="last_lightning_strike_event", + restore_last_value=True, translation_key="lightning_strike_last_distance", state_class=SensorStateClass.MEASUREMENT, device_class=SensorDeviceClass.DISTANCE, @@ -175,6 +181,7 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="lightning_strike_last_energy", device_attr="last_lightning_strike_event", + restore_last_value=True, translation_key="lightning_strike_last_energy", state_class=SensorStateClass.MEASUREMENT, none_is_unknown=True, @@ -184,6 +191,7 @@ SENSORS: tuple[WeatherFlowSensorEntityDescription, ...] = ( WeatherFlowSensorEntityDescription( key="lightning_strike_last_epoch", device_attr="last_lightning_strike_event", + restore_last_value=True, translation_key="lightning_strike_last_epoch", device_class=SensorDeviceClass.TIMESTAMP, none_is_unknown=True, @@ -358,7 +366,7 @@ async def async_setup_entry( ) -class WeatherFlowSensorEntity(SensorEntity): +class WeatherFlowSensorEntity(RestoreSensor): """Defines a WeatherFlow sensor entity.""" entity_description: WeatherFlowSensorEntityDescription @@ -404,8 +412,17 @@ class WeatherFlowSensorEntity(SensorEntity): @override async def async_added_to_hass(self) -> None: - """Subscribe to events.""" - self._async_update_state() + """Restore the last value if configured, then subscribe to events.""" + if ( + self.entity_description.restore_last_value + and self.entity_description.get_raw_value(self.device) is None + and (last_sensor_data := await self.async_get_last_sensor_data()) + and last_sensor_data.native_value is not None + ): + # Strikes are only reported as events, so keep the last one. + self._attr_native_value = last_sensor_data.native_value + else: + self._async_update_state() for event in self.entity_description.event_subscriptions: self.async_on_remove( self.device.on(event, lambda _: self._async_update_state()) diff --git a/tests/components/weatherflow/fixtures/evt_strike.json b/tests/components/weatherflow/fixtures/evt_strike.json new file mode 100644 index 000000000000..ad8f6a6dd9a7 --- /dev/null +++ b/tests/components/weatherflow/fixtures/evt_strike.json @@ -0,0 +1,6 @@ +{ + "serial_number": "ST-00000001", + "type": "evt_strike", + "hub_sn": "HB-00000001", + "evt": [1510855990, 27, 3848] +} diff --git a/tests/components/weatherflow/test_sensor.py b/tests/components/weatherflow/test_sensor.py index ca0f1be82168..02edaadaed0a 100644 --- a/tests/components/weatherflow/test_sensor.py +++ b/tests/components/weatherflow/test_sensor.py @@ -7,7 +7,7 @@ from pyweatherflowudp.aioudp import LocalEndpoint from syrupy.assertion import SnapshotAssertion from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, Platform -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.util.unit_system import ( METRIC_SYSTEM, @@ -17,10 +17,16 @@ from homeassistant.util.unit_system import ( from . import HUB_ADDRESS, setup_integration -from tests.common import MockConfigEntry, load_fixture_bytes, snapshot_platform +from tests.common import ( + MockConfigEntry, + load_fixture_bytes, + mock_restore_cache_with_extra_data, + snapshot_platform, +) LAST_STRIKE_DISTANCE = "sensor.st_00000001_lightning_last_distance" LAST_STRIKE_ENERGY = "sensor.st_00000001_lightning_last_energy" +LAST_STRIKE_TIME = "sensor.st_00000001_lightning_last_strike" RAIN_LAST_MINUTE = "sensor.st_00000001_precipitation" STATION_PRESSURE = "sensor.st_00000001_air_pressure" VAPOR_PRESSURE = "sensor.st_00000001_vapor_pressure" @@ -67,6 +73,51 @@ async def test_unit_system( assert hass.states.get(entity_id).attributes[ATTR_UNIT_OF_MEASUREMENT] == unit +async def test_last_strike_restored_until_next_strike( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_udp_endpoint: LocalEndpoint, +) -> None: + """Test the last strike is restored after a restart until a new strike.""" + mock_restore_cache_with_extra_data( + hass, + [ + ( + State(LAST_STRIKE_DISTANCE, "5"), + {"native_value": 5, "native_unit_of_measurement": "km"}, + ), + ( + State(LAST_STRIKE_ENERGY, "1234"), + {"native_value": 1234, "native_unit_of_measurement": None}, + ), + ( + State(LAST_STRIKE_TIME, "2026-09-23T23:13:10+00:00"), + { + "native_value": { + "__type": "", + "isoformat": "2026-09-23T23:13:10+00:00", + }, + "native_unit_of_measurement": None, + }, + ), + ], + ) + await setup_integration(hass, mock_config_entry, mock_udp_endpoint) + + assert hass.states.get(LAST_STRIKE_DISTANCE).state == "5" + assert hass.states.get(LAST_STRIKE_ENERGY).state == "1234" + assert hass.states.get(LAST_STRIKE_TIME).state == "2026-09-23T23:13:10+00:00" + + mock_udp_endpoint.feed_datagram( + load_fixture_bytes("evt_strike.json", "weatherflow"), HUB_ADDRESS + ) + await hass.async_block_till_done() + + assert hass.states.get(LAST_STRIKE_DISTANCE).state == "27" + assert hass.states.get(LAST_STRIKE_ENERGY).state == "3848" + assert hass.states.get(LAST_STRIKE_TIME).state == "2017-11-16T18:13:10+00:00" + + async def test_last_strike_out_of_range( hass: HomeAssistant, mock_config_entry: MockConfigEntry, @@ -82,3 +133,33 @@ async def test_last_strike_out_of_range( assert hass.states.get(LAST_STRIKE_DISTANCE).state == STATE_UNKNOWN assert hass.states.get(LAST_STRIKE_ENERGY).state == "3848" + + +async def test_last_strike_out_of_range_not_restored( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_udp_endpoint: LocalEndpoint, +) -> None: + """Test a strike cached before the sensors are added isn't replaced on restore.""" + mock_restore_cache_with_extra_data( + hass, + [ + ( + State(LAST_STRIKE_DISTANCE, "5"), + {"native_value": 5, "native_unit_of_measurement": "km"}, + ), + ], + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # The sensors are added once the device status and an observation have arrived. + for fixture in ("evt_strike_out_of_range.json", "device.json", "obs_st.json"): + mock_udp_endpoint.feed_datagram( + load_fixture_bytes(fixture, "weatherflow"), HUB_ADDRESS + ) + await hass.async_block_till_done() + + assert hass.states.get(LAST_STRIKE_DISTANCE).state == STATE_UNKNOWN + assert hass.states.get(LAST_STRIKE_ENERGY).state == "3848"