From 6a6054afeedd4c8c0b1a2da2a9dc62cab7223450 Mon Sep 17 00:00:00 2001 From: Andrea Turri Date: Thu, 30 Oct 2025 07:53:54 +0100 Subject: [PATCH] Miele RestoreSensor: restore native value rather than stringified state (#152750) Co-authored-by: Erik Montnemery Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com> --- homeassistant/components/miele/sensor.py | 60 +++++++++++------------- tests/components/miele/test_sensor.py | 26 +++++++++- 2 files changed, 53 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index d66e29d8f46..89765622e90 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -19,7 +19,6 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( PERCENTAGE, REVOLUTIONS_PER_MINUTE, - STATE_UNKNOWN, EntityCategory, UnitOfEnergy, UnitOfTemperature, @@ -762,40 +761,35 @@ class MieleSensor(MieleEntity, SensorEntity): class MieleRestorableSensor(MieleSensor, RestoreSensor): """Representation of a Sensor whose internal state can be restored.""" - _last_value: StateType - - def __init__( - self, - coordinator: MieleDataUpdateCoordinator, - device_id: str, - description: MieleSensorDescription, - ) -> None: - """Initialize the sensor.""" - super().__init__(coordinator, device_id, description) - self._last_value = None + _attr_native_value: StateType async def async_added_to_hass(self) -> None: """When entity is added to hass.""" await super().async_added_to_hass() # recover last value from cache when adding entity - last_value = await self.async_get_last_state() - if last_value and last_value.state != STATE_UNKNOWN: - self._last_value = last_value.state + last_data = await self.async_get_last_sensor_data() + if last_data: + self._attr_native_value = last_data.native_value # type: ignore[assignment] @property def native_value(self) -> StateType: - """Return the state of the sensor.""" - return self._last_value + """Return the state of the sensor. - def _update_last_value(self) -> None: - """Update the last value of the sensor.""" - self._last_value = self.entity_description.value_fn(self.device) + It is necessary to override `native_value` to fall back to the default + attribute-based implementation, instead of the function-based + implementation in `MieleSensor`. + """ + return self._attr_native_value + + def _update_native_value(self) -> None: + """Update the native value attribute of the sensor.""" + self._attr_native_value = self.entity_description.value_fn(self.device) @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._update_last_value() + self._update_native_value() super()._handle_coordinator_update() @@ -912,7 +906,7 @@ class MieleProgramIdSensor(MieleSensor): class MieleTimeSensor(MieleRestorableSensor): """Representation of time sensors keeping state from cache.""" - def _update_last_value(self) -> None: + def _update_native_value(self) -> None: """Update the last value of the sensor.""" current_value = self.entity_description.value_fn(self.device) @@ -923,7 +917,9 @@ class MieleTimeSensor(MieleRestorableSensor): current_status == StateStatus.PROGRAM_ENDED and self.entity_description.end_value_fn is not None ): - self._last_value = self.entity_description.end_value_fn(self._last_value) + self._attr_native_value = self.entity_description.end_value_fn( + self._attr_native_value + ) # keep value when program ends if no function is specified elif current_status == StateStatus.PROGRAM_ENDED: @@ -931,11 +927,11 @@ class MieleTimeSensor(MieleRestorableSensor): # force unknown when appliance is not working (some devices are keeping last value until a new cycle starts) elif current_status in (StateStatus.OFF, StateStatus.ON, StateStatus.IDLE): - self._last_value = None + self._attr_native_value = None # otherwise, cache value and return it else: - self._last_value = current_value + self._attr_native_value = current_value class MieleConsumptionSensor(MieleRestorableSensor): @@ -943,13 +939,13 @@ class MieleConsumptionSensor(MieleRestorableSensor): _is_reporting: bool = False - def _update_last_value(self) -> None: + def _update_native_value(self) -> None: """Update the last value of the sensor.""" current_value = self.entity_description.value_fn(self.device) current_status = StateStatus(self.device.state_status) last_value = ( - float(cast(str, self._last_value)) - if self._last_value is not None and self._last_value != STATE_UNKNOWN + float(cast(str, self._attr_native_value)) + if self._attr_native_value is not None else 0 ) @@ -963,7 +959,7 @@ class MieleConsumptionSensor(MieleRestorableSensor): StateStatus.SERVICE, ): self._is_reporting = False - self._last_value = None + self._attr_native_value = None # appliance might report the last value for consumption of previous cycle and it will report 0 # only after a while, so it is necessary to force 0 until we see the 0 value coming from API, unless @@ -973,7 +969,7 @@ class MieleConsumptionSensor(MieleRestorableSensor): and not self._is_reporting and last_value > 0 ): - self._last_value = current_value + self._attr_native_value = current_value self._is_reporting = True elif ( @@ -982,12 +978,12 @@ class MieleConsumptionSensor(MieleRestorableSensor): and current_value is not None and cast(int, current_value) > 0 ): - self._last_value = 0 + self._attr_native_value = 0 # keep value when program ends elif current_status == StateStatus.PROGRAM_ENDED: pass else: - self._last_value = current_value + self._attr_native_value = current_value self._is_reporting = True diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index d8c054683fa..642e69e4f1f 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -10,13 +10,15 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components.miele.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.core import HomeAssistant +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from tests.common import ( MockConfigEntry, async_fire_time_changed, async_load_json_object_fixture, + mock_restore_cache_with_extra_data, snapshot_platform, ) @@ -583,6 +585,7 @@ async def test_laundry_dry_scenario( check_sensor_state(hass, "sensor.tumble_dryer_elapsed_time", "20", step) +@pytest.mark.parametrize("restore_state", ["45", STATE_UNKNOWN, STATE_UNAVAILABLE]) @pytest.mark.parametrize("load_device_file", ["laundry.json"]) @pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) async def test_elapsed_time_sensor_restored( @@ -592,6 +595,7 @@ async def test_elapsed_time_sensor_restored( setup_platform: None, device_fixture: MieleDevices, freezer: FrozenDateTimeFactory, + restore_state, ) -> None: """Test that elapsed time returns the restored value when program ended.""" @@ -648,6 +652,26 @@ async def test_elapsed_time_sensor_restored( assert hass.states.get(entity_id).state == "unavailable" + # simulate restore with state different from native value + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + entity_id, + restore_state, + { + "unit_of_measurement": "min", + }, + ), + { + "native_value": "12", + "native_unit_of_measurement": "min", + }, + ), + ], + ) + await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done()