Add change service to timer (#84775)

* Add change service

* test subtract

* Test no change if timer not running

* Modify example

* Raise

* Finalize

* test event

* Fix tests

* Fix tracking time
pull/93166/head^2
G Johansson 2023-05-21 10:11:08 +02:00 committed by GitHub
parent fc7a421a48
commit ddb9a6e33c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 217 additions and 22 deletions

View File

@ -17,6 +17,7 @@ from homeassistant.const import (
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import collection
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_component import EntityComponent
@ -50,6 +51,7 @@ STATUS_PAUSED = "paused"
EVENT_TIMER_FINISHED = "timer.finished"
EVENT_TIMER_CANCELLED = "timer.cancelled"
EVENT_TIMER_CHANGED = "timer.changed"
EVENT_TIMER_STARTED = "timer.started"
EVENT_TIMER_RESTARTED = "timer.restarted"
EVENT_TIMER_PAUSED = "timer.paused"
@ -57,6 +59,7 @@ EVENT_TIMER_PAUSED = "timer.paused"
SERVICE_START = "start"
SERVICE_PAUSE = "pause"
SERVICE_CANCEL = "cancel"
SERVICE_CHANGE = "change"
SERVICE_FINISH = "finish"
STORAGE_KEY = DOMAIN
@ -158,6 +161,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component.async_register_entity_service(SERVICE_PAUSE, {}, "async_pause")
component.async_register_entity_service(SERVICE_CANCEL, {}, "async_cancel")
component.async_register_entity_service(SERVICE_FINISH, {}, "async_finish")
component.async_register_entity_service(
SERVICE_CHANGE,
{vol.Optional(ATTR_DURATION, default=DEFAULT_DURATION): cv.time_period},
"async_change",
)
return True
@ -321,6 +329,25 @@ class Timer(collection.CollectionEntity, RestoreEntity):
)
self.async_write_ha_state()
@callback
def async_change(self, duration: timedelta) -> None:
"""Change duration of a running timer."""
if self._listener is None or self._end is None:
raise HomeAssistantError(
f"Timer {self.entity_id} is not running, only active timers can be changed"
)
self._listener()
self._listener = None
self._end += duration
self._duration += duration
self._remaining = self._end - dt_util.utcnow().replace(microsecond=0)
self.hass.bus.async_fire(EVENT_TIMER_CHANGED, {ATTR_ENTITY_ID: self.entity_id})
self._listener = async_track_point_in_utc_time(
self.hass, self._async_finished, self._end
)
self.async_write_ha_state()
@callback
def async_pause(self):
"""Pause a timer."""

View File

@ -33,3 +33,18 @@ finish:
target:
entity:
domain: timer
change:
name: Change
description: Change a timer
target:
entity:
domain: timer
fields:
duration:
description: Duration to add or subtract to the running timer
default: 0
required: true
example: "00:01:00, 60 or -60"
selector:
text:

View File

@ -17,11 +17,13 @@ from homeassistant.components.timer import (
DEFAULT_DURATION,
DOMAIN,
EVENT_TIMER_CANCELLED,
EVENT_TIMER_CHANGED,
EVENT_TIMER_FINISHED,
EVENT_TIMER_PAUSED,
EVENT_TIMER_RESTARTED,
EVENT_TIMER_STARTED,
SERVICE_CANCEL,
SERVICE_CHANGE,
SERVICE_FINISH,
SERVICE_PAUSE,
SERVICE_START,
@ -43,7 +45,7 @@ from homeassistant.const import (
SERVICE_RELOAD,
)
from homeassistant.core import Context, CoreState, HomeAssistant, State
from homeassistant.exceptions import Unauthorized
from homeassistant.exceptions import HomeAssistantError, Unauthorized
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.restore_state import (
DATA_RESTORE_STATE_TASK,
@ -60,7 +62,7 @@ _LOGGER = logging.getLogger(__name__)
@pytest.fixture
def storage_setup(hass, hass_storage):
def storage_setup(hass: HomeAssistant, hass_storage):
"""Storage setup."""
async def _storage(items=None, config=None):
@ -168,26 +170,91 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
hass.bus.async_listen(EVENT_TIMER_PAUSED, fake_event_listener)
hass.bus.async_listen(EVENT_TIMER_FINISHED, fake_event_listener)
hass.bus.async_listen(EVENT_TIMER_CANCELLED, fake_event_listener)
hass.bus.async_listen(EVENT_TIMER_CHANGED, fake_event_listener)
steps = [
{"call": SERVICE_START, "state": STATUS_ACTIVE, "event": EVENT_TIMER_STARTED},
{"call": SERVICE_PAUSE, "state": STATUS_PAUSED, "event": EVENT_TIMER_PAUSED},
{"call": SERVICE_START, "state": STATUS_ACTIVE, "event": EVENT_TIMER_RESTARTED},
{"call": SERVICE_CANCEL, "state": STATUS_IDLE, "event": EVENT_TIMER_CANCELLED},
{"call": SERVICE_START, "state": STATUS_ACTIVE, "event": EVENT_TIMER_STARTED},
{"call": SERVICE_FINISH, "state": STATUS_IDLE, "event": EVENT_TIMER_FINISHED},
{"call": SERVICE_START, "state": STATUS_ACTIVE, "event": EVENT_TIMER_STARTED},
{"call": SERVICE_PAUSE, "state": STATUS_PAUSED, "event": EVENT_TIMER_PAUSED},
{"call": SERVICE_CANCEL, "state": STATUS_IDLE, "event": EVENT_TIMER_CANCELLED},
{"call": SERVICE_START, "state": STATUS_ACTIVE, "event": EVENT_TIMER_STARTED},
{"call": SERVICE_START, "state": STATUS_ACTIVE, "event": EVENT_TIMER_RESTARTED},
{
"call": SERVICE_START,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_STARTED,
"data": {},
},
{
"call": SERVICE_PAUSE,
"state": STATUS_PAUSED,
"event": EVENT_TIMER_PAUSED,
"data": {},
},
{
"call": SERVICE_START,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_RESTARTED,
"data": {},
},
{
"call": SERVICE_CANCEL,
"state": STATUS_IDLE,
"event": EVENT_TIMER_CANCELLED,
"data": {},
},
{
"call": SERVICE_START,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_STARTED,
"data": {},
},
{
"call": SERVICE_FINISH,
"state": STATUS_IDLE,
"event": EVENT_TIMER_FINISHED,
"data": {},
},
{
"call": SERVICE_START,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_STARTED,
"data": {},
},
{
"call": SERVICE_PAUSE,
"state": STATUS_PAUSED,
"event": EVENT_TIMER_PAUSED,
"data": {},
},
{
"call": SERVICE_CANCEL,
"state": STATUS_IDLE,
"event": EVENT_TIMER_CANCELLED,
"data": {},
},
{
"call": SERVICE_START,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_STARTED,
"data": {},
},
{
"call": SERVICE_CHANGE,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_CHANGED,
"data": {CONF_DURATION: 15},
},
{
"call": SERVICE_START,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_RESTARTED,
"data": {},
},
]
expectedEvents = 0
expected_events = 0
for step in steps:
if step["call"] is not None:
await hass.services.async_call(
DOMAIN, step["call"], {CONF_ENTITY_ID: "timer.test1"}
DOMAIN,
step["call"],
{CONF_ENTITY_ID: "timer.test1", **step["data"]},
blocking=True,
)
await hass.async_block_till_done()
@ -197,9 +264,9 @@ async def test_methods_and_events(hass: HomeAssistant) -> None:
assert state.state == step["state"]
if step["event"] is not None:
expectedEvents += 1
expected_events += 1
assert results[-1].event_type == step["event"]
assert len(results) == expectedEvents
assert len(results) == expected_events
async def test_start_service(hass: HomeAssistant) -> None:
@ -212,7 +279,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
assert state.attributes[ATTR_DURATION] == "0:00:10"
await hass.services.async_call(
DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"}
DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"}, blocking=True
)
await hass.async_block_till_done()
state = hass.states.get("timer.test1")
@ -222,7 +289,7 @@ async def test_start_service(hass: HomeAssistant) -> None:
assert state.attributes[ATTR_REMAINING] == "0:00:10"
await hass.services.async_call(
DOMAIN, SERVICE_CANCEL, {CONF_ENTITY_ID: "timer.test1"}
DOMAIN, SERVICE_CANCEL, {CONF_ENTITY_ID: "timer.test1"}, blocking=True
)
await hass.async_block_till_done()
state = hass.states.get("timer.test1")
@ -231,8 +298,20 @@ async def test_start_service(hass: HomeAssistant) -> None:
assert state.attributes[ATTR_DURATION] == "0:00:10"
assert ATTR_REMAINING not in state.attributes
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
DOMAIN,
SERVICE_CHANGE,
{CONF_ENTITY_ID: "timer.test1", CONF_DURATION: 10},
blocking=True,
)
await hass.async_block_till_done()
await hass.services.async_call(
DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1", CONF_DURATION: 15}
DOMAIN,
SERVICE_START,
{CONF_ENTITY_ID: "timer.test1", CONF_DURATION: 15},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("timer.test1")
@ -241,6 +320,57 @@ async def test_start_service(hass: HomeAssistant) -> None:
assert state.attributes[ATTR_DURATION] == "0:00:15"
assert state.attributes[ATTR_REMAINING] == "0:00:15"
await hass.services.async_call(
DOMAIN,
SERVICE_CHANGE,
{CONF_ENTITY_ID: "timer.test1", CONF_DURATION: 15},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("timer.test1")
assert state
assert state.state == STATUS_ACTIVE
assert state.attributes[ATTR_DURATION] == "0:00:30"
assert state.attributes[ATTR_REMAINING] == "0:00:30"
await hass.services.async_call(
DOMAIN,
SERVICE_CHANGE,
{CONF_ENTITY_ID: "timer.test1", CONF_DURATION: -10},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("timer.test1")
assert state
assert state.state == STATUS_ACTIVE
assert state.attributes[ATTR_DURATION] == "0:00:20"
assert state.attributes[ATTR_REMAINING] == "0:00:20"
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
assert state.state == STATUS_IDLE
assert state.attributes[ATTR_DURATION] == "0:00:20"
assert ATTR_REMAINING not in state.attributes
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
DOMAIN,
SERVICE_CHANGE,
{CONF_ENTITY_ID: "timer.test1", CONF_DURATION: 10},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("timer.test1")
assert state
assert state.state == STATUS_IDLE
assert state.attributes[ATTR_DURATION] == "0:00:20"
assert ATTR_REMAINING not in state.attributes
async def test_wait_till_timer_expires(hass: HomeAssistant) -> None:
"""Test for a timer to end."""
@ -262,9 +392,10 @@ async def test_wait_till_timer_expires(hass: HomeAssistant) -> None:
hass.bus.async_listen(EVENT_TIMER_PAUSED, fake_event_listener)
hass.bus.async_listen(EVENT_TIMER_FINISHED, fake_event_listener)
hass.bus.async_listen(EVENT_TIMER_CANCELLED, fake_event_listener)
hass.bus.async_listen(EVENT_TIMER_CHANGED, fake_event_listener)
await hass.services.async_call(
DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"}
DOMAIN, SERVICE_START, {CONF_ENTITY_ID: "timer.test1"}, blocking=True
)
await hass.async_block_till_done()
@ -275,15 +406,37 @@ async def test_wait_till_timer_expires(hass: HomeAssistant) -> None:
assert results[-1].event_type == EVENT_TIMER_STARTED
assert len(results) == 1
await hass.services.async_call(
DOMAIN,
SERVICE_CHANGE,
{CONF_ENTITY_ID: "timer.test1", CONF_DURATION: 10},
blocking=True,
)
await hass.async_block_till_done()
state = hass.states.get("timer.test1")
assert state
assert state.state == STATUS_ACTIVE
assert results[-1].event_type == EVENT_TIMER_CHANGED
assert len(results) == 2
async_fire_time_changed(hass, utcnow() + timedelta(seconds=10))
await hass.async_block_till_done()
state = hass.states.get("timer.test1")
assert state
assert state.state == STATUS_ACTIVE
async_fire_time_changed(hass, utcnow() + timedelta(seconds=20))
await hass.async_block_till_done()
state = hass.states.get("timer.test1")
assert state
assert state.state == STATUS_IDLE
assert results[-1].event_type == EVENT_TIMER_FINISHED
assert len(results) == 2
assert len(results) == 3
async def test_no_initial_state_and_no_restore_state(hass: HomeAssistant) -> None: