Provide elapsed time sensor consistent in Miele (#145093)

Co-authored-by: Robert Resch <robert@resch.dev>
This commit is contained in:
Andrea Turri
2025-08-23 20:50:19 +02:00
committed by GitHub
parent 4ff2da7553
commit f0e5325510
4 changed files with 647 additions and 6 deletions

View File

@@ -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()

View File

@@ -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
}
}
}

View File

@@ -2723,7 +2723,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
'state': 'unknown',
})
# ---
# name: test_sensor_states[platforms0][sensor.oven_program-entry]
@@ -3729,7 +3729,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
'state': 'unknown',
})
# ---
# name: test_sensor_states[platforms0][sensor.washing_machine_energy_consumption-entry]
@@ -5875,7 +5875,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
'state': 'unknown',
})
# ---
# name: test_sensor_states_api_push[platforms0][sensor.washing_machine_energy_consumption-entry]
@@ -6771,7 +6771,7 @@
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0',
'state': 'unknown',
})
# ---
# name: test_vacuum_sensor_states[platforms0-vacuum_device.json][sensor.robot_vacuum_cleaner_program-entry]

View File

@@ -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"