mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Allow timer state to be restored on restart (#67658)
This commit is contained in:
parent
4d59cb290c
commit
df4ddc6491
@ -9,6 +9,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_EDITABLE,
|
ATTR_EDITABLE,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
CONF_ICON,
|
CONF_ICON,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@ -31,10 +32,16 @@ DOMAIN = "timer"
|
|||||||
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
||||||
|
|
||||||
DEFAULT_DURATION = 0
|
DEFAULT_DURATION = 0
|
||||||
|
DEFAULT_RESTORE = False
|
||||||
|
|
||||||
ATTR_DURATION = "duration"
|
ATTR_DURATION = "duration"
|
||||||
ATTR_REMAINING = "remaining"
|
ATTR_REMAINING = "remaining"
|
||||||
ATTR_FINISHES_AT = "finishes_at"
|
ATTR_FINISHES_AT = "finishes_at"
|
||||||
|
ATTR_RESTORE = "restore"
|
||||||
|
ATTR_FINISHED_AT = "finished_at"
|
||||||
|
|
||||||
CONF_DURATION = "duration"
|
CONF_DURATION = "duration"
|
||||||
|
CONF_RESTORE = "restore"
|
||||||
|
|
||||||
STATUS_IDLE = "idle"
|
STATUS_IDLE = "idle"
|
||||||
STATUS_ACTIVE = "active"
|
STATUS_ACTIVE = "active"
|
||||||
@ -58,11 +65,13 @@ CREATE_FIELDS = {
|
|||||||
vol.Required(CONF_NAME): cv.string,
|
vol.Required(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_ICON): cv.icon,
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period,
|
vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): cv.time_period,
|
||||||
|
vol.Optional(CONF_RESTORE, default=DEFAULT_RESTORE): cv.boolean,
|
||||||
}
|
}
|
||||||
UPDATE_FIELDS = {
|
UPDATE_FIELDS = {
|
||||||
vol.Optional(CONF_NAME): cv.string,
|
vol.Optional(CONF_NAME): cv.string,
|
||||||
vol.Optional(CONF_ICON): cv.icon,
|
vol.Optional(CONF_ICON): cv.icon,
|
||||||
vol.Optional(CONF_DURATION): cv.time_period,
|
vol.Optional(CONF_DURATION): cv.time_period,
|
||||||
|
vol.Optional(CONF_RESTORE): cv.boolean,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -90,6 +99,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.All(
|
vol.Optional(CONF_DURATION, default=DEFAULT_DURATION): vol.All(
|
||||||
cv.time_period, _format_timedelta
|
cv.time_period, _format_timedelta
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_RESTORE, default=DEFAULT_RESTORE): cv.boolean,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -197,6 +207,7 @@ class Timer(RestoreEntity):
|
|||||||
self._remaining: timedelta | None = None
|
self._remaining: timedelta | None = None
|
||||||
self._end: datetime | None = None
|
self._end: datetime | None = None
|
||||||
self._listener: Callable[[], None] | None = None
|
self._listener: Callable[[], None] | None = None
|
||||||
|
self._restore: bool = self._config[CONF_RESTORE]
|
||||||
|
|
||||||
self._attr_should_poll = False
|
self._attr_should_poll = False
|
||||||
self._attr_force_update = True
|
self._attr_force_update = True
|
||||||
@ -235,6 +246,8 @@ class Timer(RestoreEntity):
|
|||||||
attrs[ATTR_FINISHES_AT] = self._end.isoformat()
|
attrs[ATTR_FINISHES_AT] = self._end.isoformat()
|
||||||
if self._remaining is not None:
|
if self._remaining is not None:
|
||||||
attrs[ATTR_REMAINING] = _format_timedelta(self._remaining)
|
attrs[ATTR_REMAINING] = _format_timedelta(self._remaining)
|
||||||
|
if self._restore:
|
||||||
|
attrs[ATTR_RESTORE] = self._restore
|
||||||
|
|
||||||
return attrs
|
return attrs
|
||||||
|
|
||||||
@ -245,15 +258,41 @@ class Timer(RestoreEntity):
|
|||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Call when entity is about to be added to Home Assistant."""
|
"""Call when entity is about to be added to Home Assistant."""
|
||||||
# If not None, we got an initial value.
|
# If we don't need to restore a previous state or no previous state exists,
|
||||||
if self._state is not None:
|
# start at idle
|
||||||
|
if not self._restore or (state := await self.async_get_last_state()) is None:
|
||||||
|
self._state = STATUS_IDLE
|
||||||
return
|
return
|
||||||
|
|
||||||
state = await self.async_get_last_state()
|
# Begin restoring state
|
||||||
self._state = state and state.state == state
|
self._state = state.state
|
||||||
|
self._duration = cv.time_period(state.attributes[ATTR_DURATION])
|
||||||
|
|
||||||
|
# Nothing more to do if the timer is idle
|
||||||
|
if self._state == STATUS_IDLE:
|
||||||
|
return
|
||||||
|
|
||||||
|
# If the timer was paused, we restore the remaining time
|
||||||
|
if self._state == STATUS_PAUSED:
|
||||||
|
self._remaining = cv.time_period(state.attributes[ATTR_REMAINING])
|
||||||
|
return
|
||||||
|
# If we get here, the timer must have been active so we need to decide what
|
||||||
|
# to do based on end time and the current time
|
||||||
|
end = cv.datetime(state.attributes[ATTR_FINISHES_AT])
|
||||||
|
# If there is time remaining in the timer, restore the remaining time then
|
||||||
|
# start the timer
|
||||||
|
if (remaining := end - dt_util.utcnow().replace(microsecond=0)) > timedelta(0):
|
||||||
|
self._remaining = remaining
|
||||||
|
self._state = STATUS_PAUSED
|
||||||
|
self.async_start()
|
||||||
|
# If the timer ended before now, finish the timer. The event will indicate
|
||||||
|
# when the timer was expected to fire.
|
||||||
|
else:
|
||||||
|
self._end = end
|
||||||
|
self.async_finish()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def async_start(self, duration: timedelta):
|
def async_start(self, duration: timedelta | None = None):
|
||||||
"""Start a timer."""
|
"""Start a timer."""
|
||||||
if self._listener:
|
if self._listener:
|
||||||
self._listener()
|
self._listener()
|
||||||
@ -274,7 +313,7 @@ class Timer(RestoreEntity):
|
|||||||
|
|
||||||
self._end = start + self._remaining
|
self._end = start + self._remaining
|
||||||
|
|
||||||
self.hass.bus.async_fire(event, {"entity_id": self.entity_id})
|
self.hass.bus.async_fire(event, {ATTR_ENTITY_ID: self.entity_id})
|
||||||
|
|
||||||
self._listener = async_track_point_in_utc_time(
|
self._listener = async_track_point_in_utc_time(
|
||||||
self.hass, self._async_finished, self._end
|
self.hass, self._async_finished, self._end
|
||||||
@ -292,7 +331,7 @@ class Timer(RestoreEntity):
|
|||||||
self._remaining = self._end - dt_util.utcnow().replace(microsecond=0)
|
self._remaining = self._end - dt_util.utcnow().replace(microsecond=0)
|
||||||
self._state = STATUS_PAUSED
|
self._state = STATUS_PAUSED
|
||||||
self._end = None
|
self._end = None
|
||||||
self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {"entity_id": self.entity_id})
|
self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {ATTR_ENTITY_ID: self.entity_id})
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -304,7 +343,9 @@ class Timer(RestoreEntity):
|
|||||||
self._state = STATUS_IDLE
|
self._state = STATUS_IDLE
|
||||||
self._end = None
|
self._end = None
|
||||||
self._remaining = None
|
self._remaining = None
|
||||||
self.hass.bus.async_fire(EVENT_TIMER_CANCELLED, {"entity_id": self.entity_id})
|
self.hass.bus.async_fire(
|
||||||
|
EVENT_TIMER_CANCELLED, {ATTR_ENTITY_ID: self.entity_id}
|
||||||
|
)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -316,10 +357,14 @@ class Timer(RestoreEntity):
|
|||||||
if self._listener:
|
if self._listener:
|
||||||
self._listener()
|
self._listener()
|
||||||
self._listener = None
|
self._listener = None
|
||||||
|
end = self._end
|
||||||
self._state = STATUS_IDLE
|
self._state = STATUS_IDLE
|
||||||
self._end = None
|
self._end = None
|
||||||
self._remaining = None
|
self._remaining = None
|
||||||
self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id})
|
self.hass.bus.async_fire(
|
||||||
|
EVENT_TIMER_FINISHED,
|
||||||
|
{ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()},
|
||||||
|
)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
@ -330,13 +375,18 @@ class Timer(RestoreEntity):
|
|||||||
|
|
||||||
self._listener = None
|
self._listener = None
|
||||||
self._state = STATUS_IDLE
|
self._state = STATUS_IDLE
|
||||||
|
end = self._end
|
||||||
self._end = None
|
self._end = None
|
||||||
self._remaining = None
|
self._remaining = None
|
||||||
self.hass.bus.async_fire(EVENT_TIMER_FINISHED, {"entity_id": self.entity_id})
|
self.hass.bus.async_fire(
|
||||||
|
EVENT_TIMER_FINISHED,
|
||||||
|
{ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()},
|
||||||
|
)
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_update_config(self, config: dict) -> None:
|
async def async_update_config(self, config: dict) -> None:
|
||||||
"""Handle when the config is updated."""
|
"""Handle when the config is updated."""
|
||||||
self._config = config
|
self._config = config
|
||||||
self._duration = cv.time_period_str(config[CONF_DURATION])
|
self._duration = cv.time_period_str(config[CONF_DURATION])
|
||||||
|
self._restore = config[CONF_RESTORE]
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
@ -8,10 +8,13 @@ import pytest
|
|||||||
|
|
||||||
from homeassistant.components.timer import (
|
from homeassistant.components.timer import (
|
||||||
ATTR_DURATION,
|
ATTR_DURATION,
|
||||||
|
ATTR_FINISHES_AT,
|
||||||
ATTR_REMAINING,
|
ATTR_REMAINING,
|
||||||
|
ATTR_RESTORE,
|
||||||
CONF_DURATION,
|
CONF_DURATION,
|
||||||
CONF_ICON,
|
CONF_ICON,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
|
CONF_RESTORE,
|
||||||
DEFAULT_DURATION,
|
DEFAULT_DURATION,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_TIMER_CANCELLED,
|
EVENT_TIMER_CANCELLED,
|
||||||
@ -26,6 +29,7 @@ from homeassistant.components.timer import (
|
|||||||
STATUS_ACTIVE,
|
STATUS_ACTIVE,
|
||||||
STATUS_IDLE,
|
STATUS_IDLE,
|
||||||
STATUS_PAUSED,
|
STATUS_PAUSED,
|
||||||
|
Timer,
|
||||||
_format_timedelta,
|
_format_timedelta,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -35,16 +39,22 @@ from homeassistant.const import (
|
|||||||
ATTR_ID,
|
ATTR_ID,
|
||||||
ATTR_NAME,
|
ATTR_NAME,
|
||||||
CONF_ENTITY_ID,
|
CONF_ENTITY_ID,
|
||||||
|
CONF_ID,
|
||||||
EVENT_STATE_CHANGED,
|
EVENT_STATE_CHANGED,
|
||||||
SERVICE_RELOAD,
|
SERVICE_RELOAD,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, CoreState
|
from homeassistant.core import Context, CoreState, State
|
||||||
from homeassistant.exceptions import Unauthorized
|
from homeassistant.exceptions import Unauthorized
|
||||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||||
|
from homeassistant.helpers.restore_state import (
|
||||||
|
DATA_RESTORE_STATE_TASK,
|
||||||
|
RestoreStateData,
|
||||||
|
StoredState,
|
||||||
|
)
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util.dt import utcnow
|
from homeassistant.util.dt import utcnow
|
||||||
|
|
||||||
from tests.common import async_fire_time_changed
|
from tests.common import async_capture_events, async_fire_time_changed
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -64,6 +74,7 @@ def storage_setup(hass, hass_storage):
|
|||||||
ATTR_ID: "from_storage",
|
ATTR_ID: "from_storage",
|
||||||
ATTR_NAME: "timer from storage",
|
ATTR_NAME: "timer from storage",
|
||||||
ATTR_DURATION: "0:00:00",
|
ATTR_DURATION: "0:00:00",
|
||||||
|
ATTR_RESTORE: False,
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -580,6 +591,7 @@ async def test_update(hass, hass_ws_client, storage_setup):
|
|||||||
"type": f"{DOMAIN}/update",
|
"type": f"{DOMAIN}/update",
|
||||||
f"{DOMAIN}_id": f"{timer_id}",
|
f"{DOMAIN}_id": f"{timer_id}",
|
||||||
CONF_DURATION: 33,
|
CONF_DURATION: 33,
|
||||||
|
CONF_RESTORE: True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
resp = await client.receive_json()
|
resp = await client.receive_json()
|
||||||
@ -587,6 +599,7 @@ async def test_update(hass, hass_ws_client, storage_setup):
|
|||||||
|
|
||||||
state = hass.states.get(timer_entity_id)
|
state = hass.states.get(timer_entity_id)
|
||||||
assert state.attributes[ATTR_DURATION] == _format_timedelta(cv.time_period(33))
|
assert state.attributes[ATTR_DURATION] == _format_timedelta(cv.time_period(33))
|
||||||
|
assert state.attributes[ATTR_RESTORE]
|
||||||
|
|
||||||
|
|
||||||
async def test_ws_create(hass, hass_ws_client, storage_setup):
|
async def test_ws_create(hass, hass_ws_client, storage_setup):
|
||||||
@ -637,3 +650,183 @@ async def test_setup_no_config(hass, hass_admin_user):
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert count_start == len(hass.states.async_entity_ids())
|
assert count_start == len(hass.states.async_entity_ids())
|
||||||
|
|
||||||
|
|
||||||
|
async def test_restore_idle(hass):
|
||||||
|
"""Test entity restore logic when timer is idle."""
|
||||||
|
utc_now = utcnow()
|
||||||
|
stored_state = StoredState(
|
||||||
|
State(
|
||||||
|
"timer.test",
|
||||||
|
STATUS_IDLE,
|
||||||
|
{ATTR_DURATION: "0:00:30"},
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
utc_now,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = await RestoreStateData.async_get_instance(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await data.store.async_save([stored_state.as_dict()])
|
||||||
|
|
||||||
|
# Emulate a fresh load
|
||||||
|
hass.data.pop(DATA_RESTORE_STATE_TASK)
|
||||||
|
|
||||||
|
entity = Timer(
|
||||||
|
{
|
||||||
|
CONF_ID: "test",
|
||||||
|
CONF_NAME: "test",
|
||||||
|
CONF_DURATION: "0:01:00",
|
||||||
|
CONF_RESTORE: True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
entity.hass = hass
|
||||||
|
entity.entity_id = "timer.test"
|
||||||
|
|
||||||
|
await entity.async_added_to_hass()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert entity.state == STATUS_IDLE
|
||||||
|
assert entity.extra_state_attributes[ATTR_DURATION] == "0:00:30"
|
||||||
|
assert ATTR_REMAINING not in entity.extra_state_attributes
|
||||||
|
assert ATTR_FINISHES_AT not in entity.extra_state_attributes
|
||||||
|
assert entity.extra_state_attributes[ATTR_RESTORE]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_restore_paused(hass):
|
||||||
|
"""Test entity restore logic when timer is paused."""
|
||||||
|
utc_now = utcnow()
|
||||||
|
stored_state = StoredState(
|
||||||
|
State(
|
||||||
|
"timer.test",
|
||||||
|
STATUS_PAUSED,
|
||||||
|
{ATTR_DURATION: "0:00:30", ATTR_REMAINING: "0:00:15"},
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
utc_now,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = await RestoreStateData.async_get_instance(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await data.store.async_save([stored_state.as_dict()])
|
||||||
|
|
||||||
|
# Emulate a fresh load
|
||||||
|
hass.data.pop(DATA_RESTORE_STATE_TASK)
|
||||||
|
|
||||||
|
entity = Timer(
|
||||||
|
{
|
||||||
|
CONF_ID: "test",
|
||||||
|
CONF_NAME: "test",
|
||||||
|
CONF_DURATION: "0:01:00",
|
||||||
|
CONF_RESTORE: True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
entity.hass = hass
|
||||||
|
entity.entity_id = "timer.test"
|
||||||
|
|
||||||
|
await entity.async_added_to_hass()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert entity.state == STATUS_PAUSED
|
||||||
|
assert entity.extra_state_attributes[ATTR_DURATION] == "0:00:30"
|
||||||
|
assert entity.extra_state_attributes[ATTR_REMAINING] == "0:00:15"
|
||||||
|
assert ATTR_FINISHES_AT not in entity.extra_state_attributes
|
||||||
|
assert entity.extra_state_attributes[ATTR_RESTORE]
|
||||||
|
|
||||||
|
|
||||||
|
async def test_restore_active_resume(hass):
|
||||||
|
"""Test entity restore logic when timer is active and end time is after startup."""
|
||||||
|
events = async_capture_events(hass, EVENT_TIMER_RESTARTED)
|
||||||
|
assert not events
|
||||||
|
utc_now = utcnow()
|
||||||
|
finish = utc_now + timedelta(seconds=30)
|
||||||
|
simulated_utc_now = utc_now + timedelta(seconds=15)
|
||||||
|
stored_state = StoredState(
|
||||||
|
State(
|
||||||
|
"timer.test",
|
||||||
|
STATUS_ACTIVE,
|
||||||
|
{ATTR_DURATION: "0:00:30", ATTR_FINISHES_AT: finish.isoformat()},
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
utc_now,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = await RestoreStateData.async_get_instance(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await data.store.async_save([stored_state.as_dict()])
|
||||||
|
|
||||||
|
# Emulate a fresh load
|
||||||
|
hass.data.pop(DATA_RESTORE_STATE_TASK)
|
||||||
|
|
||||||
|
entity = Timer(
|
||||||
|
{
|
||||||
|
CONF_ID: "test",
|
||||||
|
CONF_NAME: "test",
|
||||||
|
CONF_DURATION: "0:01:00",
|
||||||
|
CONF_RESTORE: True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
entity.hass = hass
|
||||||
|
entity.entity_id = "timer.test"
|
||||||
|
|
||||||
|
# In patch make sure we ignore microseconds
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.timer.dt_util.utcnow",
|
||||||
|
return_value=simulated_utc_now.replace(microsecond=999),
|
||||||
|
):
|
||||||
|
await entity.async_added_to_hass()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entity.state == STATUS_ACTIVE
|
||||||
|
assert entity.extra_state_attributes[ATTR_DURATION] == "0:00:30"
|
||||||
|
assert entity.extra_state_attributes[ATTR_REMAINING] == "0:00:15"
|
||||||
|
assert entity.extra_state_attributes[ATTR_FINISHES_AT] == finish.isoformat()
|
||||||
|
assert entity.extra_state_attributes[ATTR_RESTORE]
|
||||||
|
assert len(events) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_restore_active_finished_outside_grace(hass):
|
||||||
|
"""Test entity restore logic: timer is active, ended while Home Assistant was stopped."""
|
||||||
|
events = async_capture_events(hass, EVENT_TIMER_FINISHED)
|
||||||
|
assert not events
|
||||||
|
utc_now = utcnow()
|
||||||
|
finish = utc_now + timedelta(seconds=30)
|
||||||
|
simulated_utc_now = utc_now + timedelta(seconds=46)
|
||||||
|
stored_state = StoredState(
|
||||||
|
State(
|
||||||
|
"timer.test",
|
||||||
|
STATUS_ACTIVE,
|
||||||
|
{ATTR_DURATION: "0:00:30", ATTR_FINISHES_AT: finish.isoformat()},
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
utc_now,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = await RestoreStateData.async_get_instance(hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await data.store.async_save([stored_state.as_dict()])
|
||||||
|
|
||||||
|
# Emulate a fresh load
|
||||||
|
hass.data.pop(DATA_RESTORE_STATE_TASK)
|
||||||
|
|
||||||
|
entity = Timer(
|
||||||
|
{
|
||||||
|
CONF_ID: "test",
|
||||||
|
CONF_NAME: "test",
|
||||||
|
CONF_DURATION: "0:01:00",
|
||||||
|
CONF_RESTORE: True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
entity.hass = hass
|
||||||
|
entity.entity_id = "timer.test"
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.timer.dt_util.utcnow", return_value=simulated_utc_now
|
||||||
|
):
|
||||||
|
await entity.async_added_to_hass()
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entity.state == STATUS_IDLE
|
||||||
|
assert entity.extra_state_attributes[ATTR_DURATION] == "0:00:30"
|
||||||
|
assert ATTR_REMAINING not in entity.extra_state_attributes
|
||||||
|
assert ATTR_FINISHES_AT not in entity.extra_state_attributes
|
||||||
|
assert entity.extra_state_attributes[ATTR_RESTORE]
|
||||||
|
assert len(events) == 1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user