mirror of
https://github.com/home-assistant/core.git
synced 2026-04-16 14:46:15 +02:00
Compare commits
1 Commits
dependabot
...
timer_add_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
faf663cef1 |
@@ -41,6 +41,7 @@ ATTR_REMAINING = "remaining"
|
||||
ATTR_FINISHES_AT = "finishes_at"
|
||||
ATTR_RESTORE = "restore"
|
||||
ATTR_FINISHED_AT = "finished_at"
|
||||
ATTR_LAST_ACTION = "last_action"
|
||||
|
||||
CONF_DURATION = "duration"
|
||||
CONF_RESTORE = "restore"
|
||||
@@ -202,6 +203,7 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
def __init__(self, config: ConfigType) -> None:
|
||||
"""Initialize a timer."""
|
||||
self._config: dict = config
|
||||
self._last_action: str | None = None
|
||||
self._state: str = STATUS_IDLE
|
||||
self._configured_duration = cv.time_period_str(config[CONF_DURATION])
|
||||
self._running_duration: timedelta = self._configured_duration
|
||||
@@ -249,6 +251,7 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
attrs: dict[str, Any] = {
|
||||
ATTR_DURATION: _format_timedelta(self._running_duration),
|
||||
ATTR_EDITABLE: self.editable,
|
||||
ATTR_LAST_ACTION: self._last_action,
|
||||
}
|
||||
if self._end is not None:
|
||||
attrs[ATTR_FINISHES_AT] = self._end.isoformat()
|
||||
@@ -274,6 +277,7 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
|
||||
# Begin restoring state
|
||||
self._state = state.state
|
||||
self._last_action = state.attributes.get(ATTR_LAST_ACTION)
|
||||
|
||||
# Nothing more to do if the timer is idle
|
||||
if self._state == STATUS_IDLE:
|
||||
@@ -321,8 +325,7 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
|
||||
self._end = start + self._remaining
|
||||
|
||||
self.async_write_ha_state()
|
||||
self.hass.bus.async_fire(event, {ATTR_ENTITY_ID: self.entity_id})
|
||||
self._fire_event_and_write_state(event)
|
||||
|
||||
self._listener = async_track_point_in_utc_time(
|
||||
self.hass, self._async_finished, self._end
|
||||
@@ -349,6 +352,8 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
self._listener()
|
||||
self._end += duration
|
||||
self._remaining = new_remaining
|
||||
# We don't use _fire_event_and_write_state here because we don't want to
|
||||
# update last_action
|
||||
self.async_write_ha_state()
|
||||
self.hass.bus.async_fire(EVENT_TIMER_CHANGED, {ATTR_ENTITY_ID: self.entity_id})
|
||||
self._listener = async_track_point_in_utc_time(
|
||||
@@ -366,8 +371,7 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
self._remaining = self._end - dt_util.utcnow().replace(microsecond=0)
|
||||
self._state = STATUS_PAUSED
|
||||
self._end = None
|
||||
self.async_write_ha_state()
|
||||
self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {ATTR_ENTITY_ID: self.entity_id})
|
||||
self._fire_event_and_write_state(EVENT_TIMER_PAUSED)
|
||||
|
||||
@callback
|
||||
def async_cancel(self) -> None:
|
||||
@@ -382,10 +386,7 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
self._end = None
|
||||
self._remaining = None
|
||||
self._running_duration = self._configured_duration
|
||||
self.async_write_ha_state()
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_TIMER_CANCELLED, {ATTR_ENTITY_ID: self.entity_id}
|
||||
)
|
||||
self._fire_event_and_write_state(EVENT_TIMER_CANCELLED)
|
||||
|
||||
@callback
|
||||
def async_finish(self) -> None:
|
||||
@@ -403,10 +404,8 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
self._end = None
|
||||
self._remaining = None
|
||||
self._running_duration = self._configured_duration
|
||||
self.async_write_ha_state()
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_TIMER_FINISHED,
|
||||
{ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()},
|
||||
self._fire_event_and_write_state(
|
||||
EVENT_TIMER_FINISHED, extra_attrs={ATTR_FINISHED_AT: end.isoformat()}
|
||||
)
|
||||
|
||||
@callback
|
||||
@@ -421,10 +420,8 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
self._end = None
|
||||
self._remaining = None
|
||||
self._running_duration = self._configured_duration
|
||||
self.async_write_ha_state()
|
||||
self.hass.bus.async_fire(
|
||||
EVENT_TIMER_FINISHED,
|
||||
{ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()},
|
||||
self._fire_event_and_write_state(
|
||||
EVENT_TIMER_FINISHED, extra_attrs={ATTR_FINISHED_AT: end.isoformat()}
|
||||
)
|
||||
|
||||
async def async_update_config(self, config: ConfigType) -> None:
|
||||
@@ -435,3 +432,14 @@ class Timer(collection.CollectionEntity, RestoreEntity):
|
||||
self._running_duration = self._configured_duration
|
||||
self._restore = config.get(CONF_RESTORE, DEFAULT_RESTORE)
|
||||
self.async_write_ha_state()
|
||||
|
||||
def _fire_event_and_write_state(
|
||||
self, event: str, *, extra_attrs: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
"""Fire the event and write state."""
|
||||
self._last_action = event.partition(".")[2]
|
||||
self.async_write_ha_state()
|
||||
event_data = {ATTR_ENTITY_ID: self.entity_id}
|
||||
if extra_attrs:
|
||||
event_data.update(extra_attrs)
|
||||
self.hass.bus.async_fire(event, event_data)
|
||||
|
||||
@@ -11,6 +11,7 @@ import pytest
|
||||
from homeassistant.components.timer import (
|
||||
ATTR_DURATION,
|
||||
ATTR_FINISHES_AT,
|
||||
ATTR_LAST_ACTION,
|
||||
ATTR_REMAINING,
|
||||
ATTR_RESTORE,
|
||||
CONF_DURATION,
|
||||
@@ -133,8 +134,9 @@ async def test_config_options(hass: HomeAssistant) -> None:
|
||||
|
||||
assert state_1.state == STATUS_IDLE
|
||||
assert state_1.attributes == {
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_DURATION: "0:00:00",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
assert state_2.state == STATUS_IDLE
|
||||
@@ -143,12 +145,14 @@ async def test_config_options(hass: HomeAssistant) -> None:
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FRIENDLY_NAME: "Hello World",
|
||||
ATTR_ICON: "mdi:work",
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
assert state_3.state == STATUS_IDLE
|
||||
assert state_3.attributes == {
|
||||
ATTR_DURATION: str(cv.time_period(DEFAULT_DURATION)),
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +169,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
assert state.attributes == {
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
results: list[tuple[Event, State | None]] = []
|
||||
@@ -191,6 +196,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"expected_state": STATUS_ACTIVE,
|
||||
"expected_extra_attributes": {
|
||||
ATTR_FINISHES_AT: finish_10,
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_STARTED,
|
||||
@@ -199,7 +205,10 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"call": SERVICE_PAUSE,
|
||||
"call_data": {},
|
||||
"expected_state": STATUS_PAUSED,
|
||||
"expected_extra_attributes": {ATTR_REMAINING: "0:00:10"},
|
||||
"expected_extra_attributes": {
|
||||
ATTR_LAST_ACTION: "paused",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_PAUSED,
|
||||
},
|
||||
{
|
||||
@@ -208,6 +217,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"expected_state": STATUS_ACTIVE,
|
||||
"expected_extra_attributes": {
|
||||
ATTR_FINISHES_AT: finish_10,
|
||||
ATTR_LAST_ACTION: "restarted",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_RESTARTED,
|
||||
@@ -216,14 +226,14 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"call": SERVICE_CANCEL,
|
||||
"call_data": {},
|
||||
"expected_state": STATUS_IDLE,
|
||||
"expected_extra_attributes": {},
|
||||
"expected_extra_attributes": {ATTR_LAST_ACTION: "cancelled"},
|
||||
"expected_event": EVENT_TIMER_CANCELLED,
|
||||
},
|
||||
{
|
||||
"call": SERVICE_CANCEL,
|
||||
"call_data": {},
|
||||
"expected_state": STATUS_IDLE,
|
||||
"expected_extra_attributes": {},
|
||||
"expected_extra_attributes": {ATTR_LAST_ACTION: "cancelled"},
|
||||
"expected_event": None,
|
||||
},
|
||||
{
|
||||
@@ -232,6 +242,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"expected_state": STATUS_ACTIVE,
|
||||
"expected_extra_attributes": {
|
||||
ATTR_FINISHES_AT: finish_10,
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_STARTED,
|
||||
@@ -240,14 +251,14 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"call": SERVICE_FINISH,
|
||||
"call_data": {},
|
||||
"expected_state": STATUS_IDLE,
|
||||
"expected_extra_attributes": {},
|
||||
"expected_extra_attributes": {ATTR_LAST_ACTION: "finished"},
|
||||
"expected_event": EVENT_TIMER_FINISHED,
|
||||
},
|
||||
{
|
||||
"call": SERVICE_FINISH,
|
||||
"call_data": {},
|
||||
"expected_state": STATUS_IDLE,
|
||||
"expected_extra_attributes": {},
|
||||
"expected_extra_attributes": {ATTR_LAST_ACTION: "finished"},
|
||||
"expected_event": None,
|
||||
},
|
||||
{
|
||||
@@ -256,6 +267,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"expected_state": STATUS_ACTIVE,
|
||||
"expected_extra_attributes": {
|
||||
ATTR_FINISHES_AT: finish_10,
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_STARTED,
|
||||
@@ -264,14 +276,17 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"call": SERVICE_PAUSE,
|
||||
"call_data": {},
|
||||
"expected_state": STATUS_PAUSED,
|
||||
"expected_extra_attributes": {ATTR_REMAINING: "0:00:10"},
|
||||
"expected_extra_attributes": {
|
||||
ATTR_LAST_ACTION: "paused",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_PAUSED,
|
||||
},
|
||||
{
|
||||
"call": SERVICE_CANCEL,
|
||||
"call_data": {},
|
||||
"expected_state": STATUS_IDLE,
|
||||
"expected_extra_attributes": {},
|
||||
"expected_extra_attributes": {ATTR_LAST_ACTION: "cancelled"},
|
||||
"expected_event": EVENT_TIMER_CANCELLED,
|
||||
},
|
||||
{
|
||||
@@ -280,6 +295,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"expected_state": STATUS_ACTIVE,
|
||||
"expected_extra_attributes": {
|
||||
ATTR_FINISHES_AT: finish_10,
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_STARTED,
|
||||
@@ -290,6 +306,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"expected_state": STATUS_ACTIVE,
|
||||
"expected_extra_attributes": {
|
||||
ATTR_FINISHES_AT: finish_5,
|
||||
ATTR_LAST_ACTION: "started", # Change does not set last_action
|
||||
ATTR_REMAINING: "0:00:05",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_CHANGED,
|
||||
@@ -300,6 +317,7 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"expected_state": STATUS_ACTIVE,
|
||||
"expected_extra_attributes": {
|
||||
ATTR_FINISHES_AT: finish_5,
|
||||
ATTR_LAST_ACTION: "restarted",
|
||||
ATTR_REMAINING: "0:00:05",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_RESTARTED,
|
||||
@@ -308,14 +326,17 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
|
||||
"call": SERVICE_PAUSE,
|
||||
"call_data": {},
|
||||
"expected_state": STATUS_PAUSED,
|
||||
"expected_extra_attributes": {ATTR_REMAINING: "0:00:05"},
|
||||
"expected_extra_attributes": {
|
||||
ATTR_LAST_ACTION: "paused",
|
||||
ATTR_REMAINING: "0:00:05",
|
||||
},
|
||||
"expected_event": EVENT_TIMER_PAUSED,
|
||||
},
|
||||
{
|
||||
"call": SERVICE_FINISH,
|
||||
"call_data": {},
|
||||
"expected_state": STATUS_IDLE,
|
||||
"expected_extra_attributes": {},
|
||||
"expected_extra_attributes": {ATTR_LAST_ACTION: "finished"},
|
||||
"expected_event": EVENT_TIMER_FINISHED,
|
||||
},
|
||||
]
|
||||
@@ -372,6 +393,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
|
||||
assert state.attributes == {
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
await hass.services.async_call(
|
||||
@@ -385,6 +407,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=10)).isoformat(),
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
}
|
||||
|
||||
@@ -398,6 +421,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
|
||||
assert state.attributes == {
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_LAST_ACTION: "cancelled",
|
||||
}
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
@@ -422,6 +446,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_DURATION: "0:00:15",
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=15)).isoformat(),
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:15",
|
||||
}
|
||||
|
||||
@@ -460,6 +485,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_DURATION: "0:00:15",
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=12)).isoformat(),
|
||||
ATTR_LAST_ACTION: "started", # Change does not set last_action
|
||||
ATTR_REMAINING: "0:00:12",
|
||||
}
|
||||
|
||||
@@ -476,6 +502,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_DURATION: "0:00:15",
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=14)).isoformat(),
|
||||
ATTR_LAST_ACTION: "started", # Change does not set last_action
|
||||
ATTR_REMAINING: "0:00:14",
|
||||
}
|
||||
|
||||
@@ -489,6 +516,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
|
||||
assert state.attributes == {
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_LAST_ACTION: "cancelled",
|
||||
}
|
||||
|
||||
with pytest.raises(
|
||||
@@ -508,6 +536,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
|
||||
assert state.attributes == {
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_LAST_ACTION: "cancelled", # Change does not set last_action
|
||||
}
|
||||
|
||||
|
||||
@@ -526,6 +555,7 @@ async def test_wait_till_timer_expires(
|
||||
assert state.attributes == {
|
||||
ATTR_DURATION: "0:00:20",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
results = []
|
||||
@@ -553,6 +583,7 @@ async def test_wait_till_timer_expires(
|
||||
ATTR_DURATION: "0:00:20",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=20)).isoformat(),
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:20",
|
||||
}
|
||||
|
||||
@@ -574,6 +605,7 @@ async def test_wait_till_timer_expires(
|
||||
ATTR_DURATION: "0:00:20",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=15)).isoformat(),
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:15",
|
||||
}
|
||||
|
||||
@@ -591,6 +623,7 @@ async def test_wait_till_timer_expires(
|
||||
ATTR_DURATION: "0:00:20",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=5)).isoformat(),
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:15",
|
||||
}
|
||||
|
||||
@@ -604,6 +637,7 @@ async def test_wait_till_timer_expires(
|
||||
assert state.attributes == {
|
||||
ATTR_DURATION: "0:00:20",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: "finished",
|
||||
}
|
||||
|
||||
assert results[-1].event_type == EVENT_TIMER_FINISHED
|
||||
@@ -622,6 +656,7 @@ async def test_no_initial_state_and_no_restore_state(hass: HomeAssistant) -> Non
|
||||
assert state.attributes == {
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
|
||||
@@ -668,6 +703,7 @@ async def test_config_reload(
|
||||
assert state_1.attributes == {
|
||||
ATTR_DURATION: "0:00:00",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
assert state_2.state == STATUS_IDLE
|
||||
@@ -676,6 +712,7 @@ async def test_config_reload(
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FRIENDLY_NAME: "Hello World",
|
||||
ATTR_ICON: "mdi:work",
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
with patch(
|
||||
@@ -726,12 +763,14 @@ async def test_config_reload(
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FRIENDLY_NAME: "Hello World reloaded",
|
||||
ATTR_ICON: "mdi:work-reloaded",
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
assert state_3.state == STATUS_IDLE
|
||||
assert state_3.attributes == {
|
||||
ATTR_DURATION: "0:00:00",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
|
||||
@@ -748,6 +787,7 @@ async def test_timer_restarted_event(hass: HomeAssistant) -> None:
|
||||
assert state.attributes == {
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
results = []
|
||||
@@ -774,6 +814,7 @@ async def test_timer_restarted_event(hass: HomeAssistant) -> None:
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=10)).isoformat(),
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
}
|
||||
|
||||
@@ -791,6 +832,7 @@ async def test_timer_restarted_event(hass: HomeAssistant) -> None:
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=10)).isoformat(),
|
||||
ATTR_LAST_ACTION: "restarted",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
}
|
||||
|
||||
@@ -807,6 +849,7 @@ async def test_timer_restarted_event(hass: HomeAssistant) -> None:
|
||||
assert state.attributes == {
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: "paused",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
}
|
||||
|
||||
@@ -824,6 +867,7 @@ async def test_timer_restarted_event(hass: HomeAssistant) -> None:
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=10)).isoformat(),
|
||||
ATTR_LAST_ACTION: "restarted",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
}
|
||||
|
||||
@@ -844,6 +888,7 @@ async def test_state_changed_when_timer_restarted(hass: HomeAssistant) -> None:
|
||||
assert state.attributes == {
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
results = []
|
||||
@@ -866,6 +911,7 @@ async def test_state_changed_when_timer_restarted(hass: HomeAssistant) -> None:
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=10)).isoformat(),
|
||||
ATTR_LAST_ACTION: "started",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
}
|
||||
|
||||
@@ -883,6 +929,7 @@ async def test_state_changed_when_timer_restarted(hass: HomeAssistant) -> None:
|
||||
ATTR_DURATION: "0:00:10",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_FINISHES_AT: (utcnow() + timedelta(seconds=10)).isoformat(),
|
||||
ATTR_LAST_ACTION: "restarted",
|
||||
ATTR_REMAINING: "0:00:10",
|
||||
}
|
||||
|
||||
@@ -890,6 +937,78 @@ async def test_state_changed_when_timer_restarted(hass: HomeAssistant) -> None:
|
||||
assert len(results) == 2
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-06-05 17:47:50")
|
||||
async def test_last_action_after_restarted_timer_expires(
|
||||
hass: HomeAssistant, freezer: FrozenDateTimeFactory
|
||||
) -> None:
|
||||
"""Test that last_action changes from restarted to finished when timer expires."""
|
||||
hass.set_state(CoreState.starting)
|
||||
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}})
|
||||
|
||||
# Start the timer
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Restart the timer
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("timer.test1")
|
||||
assert state.state == STATUS_ACTIVE
|
||||
assert state.attributes[ATTR_LAST_ACTION] == "restarted"
|
||||
|
||||
# Let the timer expire
|
||||
freezer.tick(15)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("timer.test1")
|
||||
assert state.state == STATUS_IDLE
|
||||
assert state.attributes[ATTR_LAST_ACTION] == "finished"
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-06-05 17:47:50")
|
||||
async def test_last_action_persists_across_config_update(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test that last_action is preserved when the timer config is updated."""
|
||||
hass.set_state(CoreState.starting)
|
||||
|
||||
await async_setup_component(hass, DOMAIN, {DOMAIN: {"test1": {CONF_DURATION: 10}}})
|
||||
|
||||
# Start and cancel to set last_action to "cancelled"
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"}, blocking=True
|
||||
)
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_CANCEL, {CONF_ENTITY_ID: "timer.test1"}, blocking=True
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("timer.test1")
|
||||
assert state.state == STATUS_IDLE
|
||||
assert state.attributes[ATTR_LAST_ACTION] == "cancelled"
|
||||
|
||||
# Reload with a new duration — last_action should persist
|
||||
with patch(
|
||||
"homeassistant.config.load_yaml_config_file",
|
||||
autospec=True,
|
||||
return_value={DOMAIN: {"test1": {CONF_DURATION: 20}}},
|
||||
):
|
||||
await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("timer.test1")
|
||||
assert state.state == STATUS_IDLE
|
||||
assert state.attributes[ATTR_DURATION] == "0:00:20"
|
||||
assert state.attributes[ATTR_LAST_ACTION] == "cancelled"
|
||||
|
||||
|
||||
async def test_load_from_storage(hass: HomeAssistant, storage_setup) -> None:
|
||||
"""Test set up from storage."""
|
||||
assert await storage_setup()
|
||||
@@ -899,6 +1018,7 @@ async def test_load_from_storage(hass: HomeAssistant, storage_setup) -> None:
|
||||
ATTR_DURATION: "0:00:00",
|
||||
ATTR_EDITABLE: True,
|
||||
ATTR_FRIENDLY_NAME: "timer from storage",
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
|
||||
@@ -912,6 +1032,7 @@ async def test_editable_state_attribute(hass: HomeAssistant, storage_setup) -> N
|
||||
ATTR_DURATION: "0:00:00",
|
||||
ATTR_EDITABLE: True,
|
||||
ATTR_FRIENDLY_NAME: "timer from storage",
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.from_yaml")
|
||||
@@ -919,6 +1040,7 @@ async def test_editable_state_attribute(hass: HomeAssistant, storage_setup) -> N
|
||||
assert state.attributes == {
|
||||
ATTR_DURATION: "0:00:00",
|
||||
ATTR_EDITABLE: False,
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
|
||||
|
||||
@@ -993,6 +1115,7 @@ async def test_update(
|
||||
ATTR_DURATION: "0:00:00",
|
||||
ATTR_EDITABLE: True,
|
||||
ATTR_FRIENDLY_NAME: "timer from storage",
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(DOMAIN, DOMAIN, timer_id) == timer_entity_id
|
||||
@@ -1028,6 +1151,7 @@ async def test_update(
|
||||
ATTR_DURATION: "0:00:33",
|
||||
ATTR_EDITABLE: True,
|
||||
ATTR_FRIENDLY_NAME: "timer from storage",
|
||||
ATTR_LAST_ACTION: None,
|
||||
ATTR_RESTORE: True,
|
||||
}
|
||||
|
||||
@@ -1067,6 +1191,7 @@ async def test_ws_create(
|
||||
ATTR_DURATION: "0:00:42",
|
||||
ATTR_EDITABLE: True,
|
||||
ATTR_FRIENDLY_NAME: "New Timer",
|
||||
ATTR_LAST_ACTION: None,
|
||||
}
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(DOMAIN, DOMAIN, timer_id) == timer_entity_id
|
||||
@@ -1092,6 +1217,46 @@ async def test_setup_no_config(hass: HomeAssistant, hass_admin_user: MockUser) -
|
||||
assert count_start == len(hass.states.async_entity_ids())
|
||||
|
||||
|
||||
@pytest.mark.parametrize("last_action", [None, "cancelled", "finished"])
|
||||
async def test_restore_idle(hass: HomeAssistant, last_action: str | None) -> None:
|
||||
"""Test entity restore logic when timer is idle."""
|
||||
utc_now = utcnow()
|
||||
attrs: dict[str, Any] = {ATTR_DURATION: "0:00:30"}
|
||||
if last_action is not None:
|
||||
attrs[ATTR_LAST_ACTION] = last_action
|
||||
stored_state = StoredState(
|
||||
State("timer.test", STATUS_IDLE, attrs),
|
||||
None,
|
||||
utc_now,
|
||||
)
|
||||
|
||||
data = async_get(hass)
|
||||
await data.store.async_save([stored_state.as_dict()])
|
||||
await data.async_load()
|
||||
|
||||
entity = Timer.from_storage(
|
||||
{
|
||||
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 == {
|
||||
# Idle timers reset to the configured duration, not the stored one
|
||||
ATTR_DURATION: "0:01:00",
|
||||
ATTR_EDITABLE: True,
|
||||
ATTR_LAST_ACTION: last_action,
|
||||
ATTR_RESTORE: True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-06-05 17:47:50")
|
||||
async def test_restore_paused(hass: HomeAssistant) -> None:
|
||||
"""Test entity restore logic when timer is paused."""
|
||||
@@ -1100,7 +1265,11 @@ async def test_restore_paused(hass: HomeAssistant) -> None:
|
||||
State(
|
||||
"timer.test",
|
||||
STATUS_PAUSED,
|
||||
{ATTR_DURATION: "0:00:30", ATTR_REMAINING: "0:00:15"},
|
||||
{
|
||||
ATTR_DURATION: "0:00:30",
|
||||
ATTR_LAST_ACTION: "paused",
|
||||
ATTR_REMAINING: "0:00:15",
|
||||
},
|
||||
),
|
||||
None,
|
||||
utc_now,
|
||||
@@ -1127,13 +1296,17 @@ async def test_restore_paused(hass: HomeAssistant) -> None:
|
||||
assert entity.extra_state_attributes == {
|
||||
ATTR_DURATION: "0:00:30",
|
||||
ATTR_EDITABLE: True,
|
||||
ATTR_LAST_ACTION: "paused",
|
||||
ATTR_REMAINING: "0:00:15",
|
||||
ATTR_RESTORE: True,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.freeze_time("2023-06-05 17:47:50")
|
||||
async def test_restore_active_resume(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize("last_action", [None, "started", "restarted"])
|
||||
async def test_restore_active_resume(
|
||||
hass: HomeAssistant, last_action: str | None
|
||||
) -> None:
|
||||
"""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
|
||||
@@ -1144,7 +1317,11 @@ async def test_restore_active_resume(hass: HomeAssistant) -> None:
|
||||
State(
|
||||
"timer.test",
|
||||
STATUS_ACTIVE,
|
||||
{ATTR_DURATION: "0:00:30", ATTR_FINISHES_AT: finish.isoformat()},
|
||||
{
|
||||
ATTR_DURATION: "0:00:30",
|
||||
ATTR_FINISHES_AT: finish.isoformat(),
|
||||
ATTR_LAST_ACTION: last_action,
|
||||
},
|
||||
),
|
||||
None,
|
||||
utc_now,
|
||||
@@ -1178,13 +1355,17 @@ async def test_restore_active_resume(hass: HomeAssistant) -> None:
|
||||
ATTR_DURATION: "0:00:30",
|
||||
ATTR_EDITABLE: True,
|
||||
ATTR_FINISHES_AT: finish.isoformat(),
|
||||
ATTR_LAST_ACTION: "restarted",
|
||||
ATTR_REMAINING: "0:00:15",
|
||||
ATTR_RESTORE: True,
|
||||
}
|
||||
assert len(events) == 1
|
||||
|
||||
|
||||
async def test_restore_active_finished_outside_grace(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize("last_action", [None, "started", "restarted"])
|
||||
async def test_restore_active_finished_outside_grace(
|
||||
hass: HomeAssistant, last_action: str | None
|
||||
) -> None:
|
||||
"""Test entity restore logic: timer is active, ended while Home Assistant was stopped."""
|
||||
events = async_capture_events(hass, EVENT_TIMER_FINISHED)
|
||||
assert not events
|
||||
@@ -1195,7 +1376,11 @@ async def test_restore_active_finished_outside_grace(hass: HomeAssistant) -> Non
|
||||
State(
|
||||
"timer.test",
|
||||
STATUS_ACTIVE,
|
||||
{ATTR_DURATION: "0:00:30", ATTR_FINISHES_AT: finish.isoformat()},
|
||||
{
|
||||
ATTR_DURATION: "0:00:30",
|
||||
ATTR_FINISHES_AT: finish.isoformat(),
|
||||
ATTR_LAST_ACTION: last_action,
|
||||
},
|
||||
),
|
||||
None,
|
||||
utc_now,
|
||||
@@ -1226,6 +1411,7 @@ async def test_restore_active_finished_outside_grace(hass: HomeAssistant) -> Non
|
||||
assert entity.extra_state_attributes == {
|
||||
ATTR_DURATION: "0:01:00",
|
||||
ATTR_EDITABLE: True,
|
||||
ATTR_LAST_ACTION: "finished",
|
||||
ATTR_RESTORE: True,
|
||||
}
|
||||
assert len(events) == 1
|
||||
|
||||
Reference in New Issue
Block a user