diff --git a/homeassistant/components/miele/sensor.py b/homeassistant/components/miele/sensor.py index 982a1198dab..988f25accdc 100644 --- a/homeassistant/components/miele/sensor.py +++ b/homeassistant/components/miele/sensor.py @@ -10,6 +10,7 @@ from typing import Any, Final, cast from pymiele import MieleDevice, MieleTemperature from homeassistant.components.sensor import ( + RestoreSensor, SensorDeviceClass, SensorEntity, SensorEntityDescription, @@ -18,13 +19,14 @@ from homeassistant.components.sensor import ( from homeassistant.const import ( PERCENTAGE, REVOLUTIONS_PER_MINUTE, + STATE_UNKNOWN, EntityCategory, UnitOfEnergy, UnitOfTemperature, UnitOfTime, UnitOfVolume, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType @@ -105,6 +107,7 @@ class MieleSensorDescription(SensorEntityDescription): """Class describing Miele sensor entities.""" value_fn: Callable[[MieleDevice], StateType] + end_value_fn: Callable[[StateType], StateType] | None = None extra_attributes: dict[str, Callable[[MieleDevice], StateType]] | None = None zone: int | None = None unique_id_fn: Callable[[str, MieleSensorDescription], str] | None = None @@ -386,6 +389,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = ( key="state_elapsed_time", translation_key="elapsed_time", value_fn=lambda value: _convert_duration(value.state_elapsed_time), + end_value_fn=lambda last_value: last_value, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, entity_category=EntityCategory.DIAGNOSTIC, @@ -609,6 +613,7 @@ async def async_setup_entry( "state_program_id": MieleProgramIdSensor, "state_program_phase": MielePhaseSensor, "state_plate_step": MielePlateSensor, + "state_elapsed_time": MieleTimeSensor, }.get(definition.description.key, MieleSensor) def _is_entity_registered(unique_id: str) -> bool: @@ -745,6 +750,36 @@ class MieleSensor(MieleEntity, SensorEntity): return attr +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 + + 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 + last_value = await self.async_get_last_state() + if last_value and last_value.state != STATE_UNKNOWN: + self._last_value = last_value.state + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self._last_value + + class MielePlateSensor(MieleSensor): """Representation of a Sensor.""" @@ -846,3 +881,35 @@ class MieleProgramIdSensor(MieleSensor): def options(self) -> list[str]: """Return the options list for the actual device type.""" return sorted(set(STATE_PROGRAM_ID.get(self.device.device_type, {}).values())) + + +class MieleTimeSensor(MieleRestorableSensor): + """Representation of time sensors keeping state from cache.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + + current_value = self.entity_description.value_fn(self.device) + current_status = StateStatus(self.device.state_status) + + # report end-specific value when program ends (some devices are immediately reporting 0...) + if ( + 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) + + # keep value when program ends if no function is specified + elif current_status == StateStatus.PROGRAM_ENDED: + pass + + # 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 + + # otherwise, cache value and return it + else: + self._last_value = current_value + + super()._handle_coordinator_update() diff --git a/tests/components/miele/fixtures/laundry.json b/tests/components/miele/fixtures/laundry.json new file mode 100644 index 00000000000..a72f2283039 --- /dev/null +++ b/tests/components/miele/fixtures/laundry.json @@ -0,0 +1,272 @@ +{ + "DummyWasher": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 1, + "value_localized": "Washing machine" + }, + "deviceName": "", + "protocolVersion": 4, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "32", + "techType": "WCR870", + "matNumber": "10979100", + "swids": [ + "5836", + "20457", + "20449", + "25260", + "20450", + "5013", + "25314", + "25205", + "25313", + "25191" + ] + }, + "xkmIdentLabel": { + "techType": "EK057", + "releaseVersion": "08.32" + } + }, + "state": { + "ProgramID": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program name" + }, + "status": { + "value_raw": 1, + "value_localized": "Off", + "key_localized": "status" + }, + "programType": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": true, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": false, + "mobileStart": false + }, + "ambientLight": null, + "light": null, + "elapsedTime": [0, 0], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } + }, + "DummyDryer": { + "ident": { + "type": { + "key_localized": "Device type", + "value_raw": 2, + "value_localized": "Tumble dryer" + }, + "deviceName": "", + "protocolVersion": 4, + "deviceIdentLabel": { + "fabNumber": "**REDACTED**", + "fabIndex": "13", + "techType": "TCJ690WP", + "matNumber": "10979980", + "swids": [ + "5213", + "25359", + "25360", + "25002", + "20456", + "25213", + "5136", + "20445", + "25234", + "4174" + ] + }, + "xkmIdentLabel": { + "techType": "EK037", + "releaseVersion": "04.05" + } + }, + "state": { + "ProgramID": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program name" + }, + "status": { + "value_raw": 1, + "value_localized": "Off", + "key_localized": "status" + }, + "programType": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program type" + }, + "programPhase": { + "value_raw": 0, + "value_localized": "", + "key_localized": "Program phase" + }, + "remainingTime": [0, 0], + "startTime": [0, 0], + "targetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTargetTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "temperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + }, + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "coreTemperature": [ + { + "value_raw": -32768, + "value_localized": null, + "unit": "Celsius" + } + ], + "signalInfo": false, + "signalFailure": false, + "signalDoor": false, + "remoteEnable": { + "fullRemoteControl": true, + "smartGrid": true, + "mobileStart": false + }, + "ambientLight": null, + "light": null, + "elapsedTime": [0, 55], + "spinningSpeed": { + "unit": "rpm", + "value_raw": null, + "value_localized": null, + "key_localized": "Spin speed" + }, + "dryingStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Drying level" + }, + "ventilationStep": { + "value_raw": null, + "value_localized": "", + "key_localized": "Fan level" + }, + "plateStep": [], + "ecoFeedback": null, + "batteryLevel": null + } + } +} diff --git a/tests/components/miele/snapshots/test_sensor.ambr b/tests/components/miele/snapshots/test_sensor.ambr index 33ec2013936..f385a53b6e4 100644 --- a/tests/components/miele/snapshots/test_sensor.ambr +++ b/tests/components/miele/snapshots/test_sensor.ambr @@ -2723,7 +2723,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unknown', }) # --- # name: test_sensor_states[platforms0][sensor.oven_program-entry] @@ -3729,7 +3729,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unknown', }) # --- # name: test_sensor_states[platforms0][sensor.washing_machine_energy_consumption-entry] @@ -5875,7 +5875,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unknown', }) # --- # name: test_sensor_states_api_push[platforms0][sensor.washing_machine_energy_consumption-entry] @@ -6771,7 +6771,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0', + 'state': 'unknown', }) # --- # name: test_vacuum_sensor_states[platforms0-vacuum_device.json][sensor.robot_vacuum_cleaner_program-entry] diff --git a/tests/components/miele/test_sensor.py b/tests/components/miele/test_sensor.py index 0fc7a891509..f8d620c8bd0 100644 --- a/tests/components/miele/test_sensor.py +++ b/tests/components/miele/test_sensor.py @@ -239,7 +239,6 @@ async def test_temperature_sensor_registry_lookup( await hass.config_entries.async_reload(mock_config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(entity_id).state == "unknown" @@ -286,3 +285,306 @@ async def test_coffee_system_sensor_states( """Test coffee system sensor state.""" await snapshot_platform(hass, entity_registry, snapshot, setup_platform.entry_id) + + +@pytest.mark.parametrize("load_device_file", ["laundry.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +async def test_laundry_wash_scenario( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, + mock_config_entry: MockConfigEntry, + device_fixture: MieleDevices, + freezer: FrozenDateTimeFactory, +) -> None: + """Parametrized test for verifying time sensors for wahsing machine devices when API glitches at program end.""" + + step = 0 + + # Initial state when the washing machine is off + check_sensor_state(hass, "sensor.washing_machine", "off", step) + check_sensor_state(hass, "sensor.washing_machine_program", "no_program", step) + check_sensor_state( + hass, "sensor.washing_machine_program_phase", "not_running", step + ) + check_sensor_state( + hass, "sensor.washing_machine_target_temperature", "unknown", step + ) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "unknown", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "0", step) + # OFF -> elapsed forced to unknown (some devices continue reporting last value of last cycle) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "unknown", step) + + # Simulate program started + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 5 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "In use" + device_fixture["DummyWasher"]["state"]["ProgramID"]["value_raw"] = 3 + device_fixture["DummyWasher"]["state"]["ProgramID"]["value_localized"] = ( + "Minimum iron" + ) + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 260 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Main wash" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 1 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 45 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0]["value_raw"] = 3000 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0][ + "value_localized" + ] = 30.0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 12 + device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_raw"] = 1200 + device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_localized"] = "1200" + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.washing_machine", "in_use", step) + check_sensor_state(hass, "sensor.washing_machine_program", "minimum_iron", step) + check_sensor_state(hass, "sensor.washing_machine_program_phase", "main_wash", step) + check_sensor_state(hass, "sensor.washing_machine_target_temperature", "30.0", step) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "1200", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "105", step) + # IN_USE -> elapsed time from API (normal case) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "12", step) + + # Simulate rinse hold phase + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 11 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "Rinse hold" + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 262 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Rinse hold" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 8 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 1 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 49 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.washing_machine", "rinse_hold", step) + check_sensor_state(hass, "sensor.washing_machine_program", "minimum_iron", step) + check_sensor_state(hass, "sensor.washing_machine_program_phase", "rinse_hold", step) + check_sensor_state(hass, "sensor.washing_machine_target_temperature", "30.0", step) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "1200", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "8", step) + # RINSE HOLD -> elapsed time from API (normal case) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "109", step) + + # Simulate program ended + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 7 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "Finished" + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 267 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Anti-crease" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 0 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.washing_machine", "program_ended", step) + check_sensor_state(hass, "sensor.washing_machine_program", "minimum_iron", step) + check_sensor_state( + hass, "sensor.washing_machine_program_phase", "anti_crease", step + ) + check_sensor_state(hass, "sensor.washing_machine_target_temperature", "30.0", step) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "1200", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "0", step) + # PROGRAM_ENDED -> elapsed time kept from last program (some devices immediately go to 0) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "109", step) + + # Simulate when door is opened after program ended + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 3 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = ( + "Programme selected" + ) + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 256 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = "" + device_fixture["DummyWasher"]["state"]["targetTemperature"][0]["value_raw"] = 4000 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0][ + "value_localized" + ] = 40.0 + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 1 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 59 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 0 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.washing_machine", "programmed", step) + check_sensor_state(hass, "sensor.washing_machine_program", "minimum_iron", step) + check_sensor_state( + hass, "sensor.washing_machine_program_phase", "not_running", step + ) + check_sensor_state(hass, "sensor.washing_machine_target_temperature", "40.0", step) + check_sensor_state(hass, "sensor.washing_machine_spin_speed", "1200", step) + check_sensor_state(hass, "sensor.washing_machine_remaining_time", "119", step) + # PROGRAMMED -> elapsed time from API (normal case) + check_sensor_state(hass, "sensor.washing_machine_elapsed_time", "0", step) + + +@pytest.mark.parametrize("load_device_file", ["laundry.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +async def test_laundry_dry_scenario( + hass: HomeAssistant, + mock_miele_client: MagicMock, + setup_platform: None, + mock_config_entry: MockConfigEntry, + device_fixture: MieleDevices, + freezer: FrozenDateTimeFactory, +) -> None: + """Parametrized test for verifying time sensors for tumble dryer devices when API reports time value from last cycle, when device is off.""" + + step = 0 + + # Initial state when the washing machine is off + check_sensor_state(hass, "sensor.tumble_dryer", "off", step) + check_sensor_state(hass, "sensor.tumble_dryer_program", "no_program", step) + check_sensor_state(hass, "sensor.tumble_dryer_program_phase", "not_running", step) + check_sensor_state(hass, "sensor.tumble_dryer_drying_step", "unknown", step) + check_sensor_state(hass, "sensor.tumble_dryer_remaining_time", "0", step) + # OFF -> elapsed forced to unknown (some devices continue reporting last value of last cycle) + check_sensor_state(hass, "sensor.tumble_dryer_elapsed_time", "unknown", step) + + # Simulate program started + device_fixture["DummyDryer"]["state"]["status"]["value_raw"] = 5 + device_fixture["DummyDryer"]["state"]["status"]["value_localized"] = "In use" + device_fixture["DummyDryer"]["state"]["ProgramID"]["value_raw"] = 3 + device_fixture["DummyDryer"]["state"]["ProgramID"]["value_localized"] = ( + "Minimum iron" + ) + device_fixture["DummyDryer"]["state"]["programPhase"]["value_raw"] = 514 + device_fixture["DummyDryer"]["state"]["programPhase"]["value_localized"] = "Drying" + device_fixture["DummyDryer"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyDryer"]["state"]["remainingTime"][1] = 49 + device_fixture["DummyDryer"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyDryer"]["state"]["elapsedTime"][1] = 20 + device_fixture["DummyDryer"]["state"]["dryingStep"]["value_raw"] = 2 + device_fixture["DummyDryer"]["state"]["dryingStep"]["value_localized"] = "Normal" + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.tumble_dryer", "in_use", step) + check_sensor_state(hass, "sensor.tumble_dryer_program", "minimum_iron", step) + check_sensor_state(hass, "sensor.tumble_dryer_program_phase", "drying", step) + check_sensor_state(hass, "sensor.tumble_dryer_drying_step", "normal", step) + check_sensor_state(hass, "sensor.tumble_dryer_remaining_time", "49", step) + # IN_USE -> elapsed time from API (normal case) + check_sensor_state(hass, "sensor.tumble_dryer_elapsed_time", "20", step) + + # Simulate program end + device_fixture["DummyDryer"]["state"]["status"]["value_raw"] = 7 + device_fixture["DummyDryer"]["state"]["status"]["value_localized"] = "Finished" + device_fixture["DummyDryer"]["state"]["programPhase"]["value_raw"] = 522 + device_fixture["DummyDryer"]["state"]["programPhase"]["value_localized"] = ( + "Finished" + ) + device_fixture["DummyDryer"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyDryer"]["state"]["remainingTime"][1] = 0 + device_fixture["DummyDryer"]["state"]["elapsedTime"][0] = 1 + device_fixture["DummyDryer"]["state"]["elapsedTime"][1] = 18 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + step += 1 + + check_sensor_state(hass, "sensor.tumble_dryer", "program_ended", step) + check_sensor_state(hass, "sensor.tumble_dryer_program", "minimum_iron", step) + check_sensor_state(hass, "sensor.tumble_dryer_program_phase", "finished", step) + check_sensor_state(hass, "sensor.tumble_dryer_drying_step", "normal", step) + check_sensor_state(hass, "sensor.tumble_dryer_remaining_time", "0", step) + # PROGRAM_ENDED -> elapsed time kept from last program (some devices immediately go to 0) + check_sensor_state(hass, "sensor.tumble_dryer_elapsed_time", "20", step) + + +@pytest.mark.parametrize("load_device_file", ["laundry.json"]) +@pytest.mark.parametrize("platforms", [(SENSOR_DOMAIN,)]) +async def test_elapsed_time_sensor_restored( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_miele_client: MagicMock, + setup_platform: None, + device_fixture: MieleDevices, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that elapsed time returns the restored value when program ended.""" + + entity_id = "sensor.washing_machine_elapsed_time" + + # Simulate program started + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 5 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "In use" + device_fixture["DummyWasher"]["state"]["ProgramID"]["value_raw"] = 3 + device_fixture["DummyWasher"]["state"]["ProgramID"]["value_localized"] = ( + "Minimum iron" + ) + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 260 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Main wash" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 1 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 45 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0]["value_raw"] = 3000 + device_fixture["DummyWasher"]["state"]["targetTemperature"][0][ + "value_localized" + ] = 30.0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 12 + device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_raw"] = 1200 + device_fixture["DummyWasher"]["state"]["spinningSpeed"]["value_localized"] = "1200" + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "12" + + # Simulate program ended + device_fixture["DummyWasher"]["state"]["status"]["value_raw"] = 7 + device_fixture["DummyWasher"]["state"]["status"]["value_localized"] = "Finished" + device_fixture["DummyWasher"]["state"]["programPhase"]["value_raw"] = 267 + device_fixture["DummyWasher"]["state"]["programPhase"]["value_localized"] = ( + "Anti-crease" + ) + device_fixture["DummyWasher"]["state"]["remainingTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["remainingTime"][1] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][0] = 0 + device_fixture["DummyWasher"]["state"]["elapsedTime"][1] = 0 + + freezer.tick(timedelta(seconds=130)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # unload config entry and reload to make sure that the state is restored + + await hass.config_entries.async_unload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get(entity_id).state == "unavailable" + + await hass.config_entries.async_reload(mock_config_entry.entry_id) + await hass.async_block_till_done() + + # check that elapsed time is the one restored and not the value reported by API (0) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "12"