Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "at_least" and "at_most" actions to the Timer integration #139049

Draft
wants to merge 2 commits into
base: dev
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions homeassistant/components/timer/__init__.py
Original file line number Diff line number Diff line change
@@ -60,6 +60,8 @@
SERVICE_PAUSE = "pause"
SERVICE_CANCEL = "cancel"
SERVICE_CHANGE = "change"
SERVICE_ATLEAST = "at_least"
SERVICE_ATMOST = "at_most"
SERVICE_FINISH = "finish"

STORAGE_KEY = DOMAIN
@@ -166,6 +168,16 @@ async def reload_service_handler(service_call: ServiceCall) -> None:
{vol.Optional(ATTR_DURATION, default=DEFAULT_DURATION): cv.time_period},
"async_change",
)
component.async_register_entity_service(
SERVICE_ATLEAST,
{vol.Optional(ATTR_DURATION, default=DEFAULT_DURATION): cv.time_period},
"async_at_least",
)
component.async_register_entity_service(
SERVICE_ATMOST,
{vol.Optional(ATTR_DURATION, default=DEFAULT_DURATION): cv.time_period},
"async_at_most",
)

return True

@@ -357,6 +369,44 @@ def async_change(self, duration: timedelta) -> None:
self.hass, self._async_finished, self._end
)

@callback
def async_at_least(self, duration: timedelta) -> None:
"""Increase the remaining time to >= duration"""
if self._state == STATUS_PAUSED:
# On a paused timer, just update the remaining time (if
# necessary).
changed = False
if self._remaining < duration:
self._remaining = duration
changed = True
if self._running_duration < duration:
self._running_duration = duration
changed = True
if changed:
self.async_write_ha_state()
return

new_end = dt_util.utcnow().replace(microsecond=0) + duration
if self._state == STATUS_ACTIVE and new_end <= self._end:
# The timer already has sufficient time remaining
return

self.async_start(duration)

@callback
def async_at_most(self, duration: timedelta) -> None:
"""Decrease the remaining time to <= duration"""
if self._state == STATUS_IDLE:
# The timer is already finished
return

new_end = dt_util.utcnow().replace(microsecond=0) + duration
if self._state == STATUS_ACTIVE and self._end <= new_end:
# The timer is already set to finish sooner.
return

self.async_start(duration)

@callback
def async_pause(self) -> None:
"""Pause a timer."""
24 changes: 24 additions & 0 deletions homeassistant/components/timer/services.yaml
Original file line number Diff line number Diff line change
@@ -37,4 +37,28 @@ change:
selector:
text:

at_least:
target:
entity:
domain: timer
fields:
duration:
default: 0
required: true
example: "00:01:00 or 60"
selector:
text:

at_most:
target:
entity:
domain: timer
fields:
duration:
default: 0
required: true
example: "00:01:00 or 60"
selector:
text:

reload:
20 changes: 20 additions & 0 deletions homeassistant/components/timer/strings.json
Original file line number Diff line number Diff line change
@@ -64,6 +64,26 @@
}
}
},
"at_least": {
"name": "At least",
"description": "Increases the remaining time if it's less than the given duration.",
"fields": {
"duration": {
"name": "Duration",
"description": "Minimum duration for the running timer."
}
}
},
"at_most": {
"name": "At most",
"description": "Decreases the remaining time if it's more than the given duration.",
"fields": {
"duration": {
"name": "Duration",
"description": "Maximum duration for the running timer."
}
}
},
"reload": {
"name": "[%key:common::action::reload%]",
"description": "Reloads timers from the YAML-configuration."
68 changes: 68 additions & 0 deletions tests/components/timer/test_init.py
Original file line number Diff line number Diff line change
@@ -24,6 +24,8 @@
EVENT_TIMER_PAUSED,
EVENT_TIMER_RESTARTED,
EVENT_TIMER_STARTED,
SERVICE_ATLEAST,
SERVICE_ATMOST,
SERVICE_CANCEL,
SERVICE_CHANGE,
SERVICE_FINISH,
@@ -268,6 +270,72 @@ def fake_event_listener(event: Event):
"event": EVENT_TIMER_FINISHED,
"data": {},
},
{
"call": SERVICE_ATMOST,
"state": STATUS_IDLE,
"event": None,
"data": {CONF_DURATION: 10},
},
{
"call": SERVICE_ATLEAST,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_STARTED,
"data": {CONF_DURATION: 10},
},
{
"call": SERVICE_ATLEAST,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_RESTARTED,
"data": {CONF_DURATION: 100},
},
{
"call": SERVICE_ATMOST,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_RESTARTED,
"data": {CONF_DURATION: 50},
},
{
"call": SERVICE_ATMOST,
"state": STATUS_ACTIVE,
"event": None,
"data": {CONF_DURATION: 100},
},
{
"call": SERVICE_ATLEAST,
"state": STATUS_ACTIVE,
"event": None,
"data": {CONF_DURATION: 10},
},
{
"call": SERVICE_PAUSE,
"state": STATUS_PAUSED,
"event": EVENT_TIMER_PAUSED,
"data": {},
},
{
"call": SERVICE_ATLEAST,
"state": STATUS_PAUSED,
"event": None,
"data": {CONF_DURATION: 20},
},
{
"call": SERVICE_ATMOST,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_RESTARTED,
"data": {CONF_DURATION: 30},
},
{
"call": SERVICE_PAUSE,
"state": STATUS_PAUSED,
"event": EVENT_TIMER_PAUSED,
"data": {},
},
{
"call": SERVICE_ATMOST,
"state": STATUS_ACTIVE,
"event": EVENT_TIMER_RESTARTED,
"data": {CONF_DURATION: 10},
},
]

expected_events = 0