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 timepull/93166/head^2
parent
fc7a421a48
commit
ddb9a6e33c
|
@ -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."""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue