mirror of
https://github.com/home-assistant/core.git
synced 2026-08-03 20:24:55 +02:00
Add button.being_held trigger
This commit is contained in:
@@ -29,6 +29,16 @@
|
||||
},
|
||||
"title": "Button",
|
||||
"triggers": {
|
||||
"being_held": {
|
||||
"description": "Triggers repeatedly, at a set interval, while one or more buttons are being held.",
|
||||
"fields": {
|
||||
"repeat_interval": {
|
||||
"description": "How often the trigger repeats while the button is held down.",
|
||||
"name": "Repeat interval"
|
||||
}
|
||||
},
|
||||
"name": "Button being held"
|
||||
},
|
||||
"double_pressed": {
|
||||
"description": "Triggers when one or more buttons are pressed twice in quick succession.",
|
||||
"name": "Button double pressed"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"""Provides triggers for buttons."""
|
||||
|
||||
import datetime
|
||||
from typing import override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.event import (
|
||||
ATTR_MULTI_PRESS_COUNT,
|
||||
DOMAIN as EVENT_DOMAIN,
|
||||
@@ -10,9 +13,13 @@ from homeassistant.components.event import (
|
||||
EventEntityStateAttribute,
|
||||
)
|
||||
from homeassistant.components.input_button import DOMAIN as INPUT_BUTTON_DOMAIN
|
||||
from homeassistant.const import CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
CONF_REPEAT_INTERVAL,
|
||||
ENTITY_STATE_TRIGGER_SCHEMA,
|
||||
NotTriggeredReasonReporter,
|
||||
StatelessEntityTriggerBase,
|
||||
Trigger,
|
||||
@@ -23,6 +30,15 @@ from . import DOMAIN
|
||||
_EVENT_BUTTON_DOMAIN_SPECS = {
|
||||
EVENT_DOMAIN: DomainSpec(device_class=EventDeviceClass.BUTTON)
|
||||
}
|
||||
_BEING_HELD_TRIGGER_SCHEMA = ENTITY_STATE_TRIGGER_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_OPTIONS, default={}): {
|
||||
vol.Required(
|
||||
CONF_REPEAT_INTERVAL, default=datetime.timedelta(seconds=1)
|
||||
): cv.positive_time_period
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ButtonPressedTrigger(StatelessEntityTriggerBase):
|
||||
@@ -104,11 +120,31 @@ class ButtonHoldEndedTrigger(StatelessEntityTriggerBase):
|
||||
)
|
||||
|
||||
|
||||
class ButtonBeingHeldTrigger(StatelessEntityTriggerBase):
|
||||
"""Trigger for button event entity being held."""
|
||||
|
||||
_domain_specs = _EVENT_BUTTON_DOMAIN_SPECS
|
||||
_schema = _BEING_HELD_TRIGGER_SCHEMA
|
||||
|
||||
@override
|
||||
def is_valid_state(
|
||||
self,
|
||||
state: State,
|
||||
report_not_triggered: NotTriggeredReasonReporter,
|
||||
) -> bool:
|
||||
"""Check if the event is a hold start."""
|
||||
return (
|
||||
state.attributes.get(EventEntityStateAttribute.EVENT_TYPE)
|
||||
== ButtonEventType.LONG_PRESS_START
|
||||
)
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"pressed": ButtonPressedTrigger,
|
||||
"double_pressed": ButtonDoublePressedTrigger,
|
||||
"hold_started": ButtonHoldStartedTrigger,
|
||||
"hold_ended": ButtonHoldEndedTrigger,
|
||||
"being_held": ButtonBeingHeldTrigger,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -16,3 +16,11 @@ hold_started:
|
||||
target: *event_button_target
|
||||
hold_ended:
|
||||
target: *event_button_target
|
||||
being_held:
|
||||
target: *event_button_target
|
||||
fields:
|
||||
repeat_interval:
|
||||
required: true
|
||||
default: 00:00:01
|
||||
selector:
|
||||
duration:
|
||||
|
||||
@@ -319,6 +319,8 @@ BEHAVIOR_FIRST: Final = "first"
|
||||
BEHAVIOR_ALL: Final = "all"
|
||||
BEHAVIOR_EACH: Final = "each"
|
||||
|
||||
CONF_REPEAT_INTERVAL: Final = "repeat_interval"
|
||||
|
||||
|
||||
def _create_deprecated_behavior_issue(deprecated: str, replacement: str) -> None:
|
||||
"""Inform the user a renamed trigger behavior value is still in use."""
|
||||
@@ -411,6 +413,9 @@ class EntityTriggerBase(Trigger):
|
||||
assert config.target is not None
|
||||
self._options = config.options or {}
|
||||
self._duration: timedelta | None = self._options.get(CONF_FOR)
|
||||
self._repeat_interval: timedelta | None = self._options.get(
|
||||
CONF_REPEAT_INTERVAL
|
||||
)
|
||||
self._target = config.target
|
||||
|
||||
def entity_filter(self, entities: set[str]) -> set[str]:
|
||||
@@ -507,7 +512,7 @@ class EntityTriggerBase(Trigger):
|
||||
pending_timers: dict[str, CALLBACK_TYPE],
|
||||
target_state_change_data: TargetStateChangedData,
|
||||
) -> None:
|
||||
"""Cancel pending duration timers invalidated by a state change.
|
||||
"""Cancel pending timers invalidated by a state change.
|
||||
|
||||
Runs on every delivered state change, before the trigger's own
|
||||
validity checks: an event which cannot fire the trigger, e.g. an
|
||||
@@ -543,7 +548,7 @@ class EntityTriggerBase(Trigger):
|
||||
entity_ids: Iterable[str],
|
||||
states: Mapping[str, State | None],
|
||||
) -> bool:
|
||||
"""Check the combined first/all state for a pending duration timer."""
|
||||
"""Check the combined first/all state for a pending timer."""
|
||||
matches, included = self.count_matches(entity_ids, states)
|
||||
if behavior == BEHAVIOR_FIRST:
|
||||
return matches >= 1
|
||||
@@ -554,8 +559,9 @@ class EntityTriggerBase(Trigger):
|
||||
# matches.
|
||||
return included > 0 and matches == included
|
||||
|
||||
# TODO: remove the noqa and sort it out
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
async def async_attach_runner( # noqa: C901
|
||||
self,
|
||||
run_action: TriggerActionRunner,
|
||||
did_not_trigger: TriggerNotTriggeredReporter | None = None,
|
||||
@@ -563,8 +569,8 @@ class EntityTriggerBase(Trigger):
|
||||
"""Attach the trigger to an action runner."""
|
||||
|
||||
behavior: str = self._options.get(ATTR_BEHAVIOR, BEHAVIOR_EACH)
|
||||
# Pending `for:` duration timers, keyed by entity_id for behavior
|
||||
# each and by the behavior for first/all.
|
||||
# Pending `for:` duration and repeat interval timers, keyed by
|
||||
# entity_id for behavior each and by the behavior for first/all.
|
||||
pending_timers: dict[str, CALLBACK_TYPE] = {}
|
||||
|
||||
@callback
|
||||
@@ -573,7 +579,7 @@ class EntityTriggerBase(Trigger):
|
||||
removed: set[str],
|
||||
entity_states: Mapping[str, State | None],
|
||||
) -> None:
|
||||
"""Re-validate pending duration timers on target changes.
|
||||
"""Re-validate pending timers on target changes.
|
||||
|
||||
Timers of entities no longer targeted are cancelled, and the
|
||||
combined first/all condition is recounted over the updated
|
||||
@@ -652,6 +658,24 @@ class EntityTriggerBase(Trigger):
|
||||
if matches != 1:
|
||||
return
|
||||
|
||||
@callback
|
||||
def start_timer(delay: timedelta) -> None:
|
||||
subscription_key = entity_id if behavior == BEHAVIOR_EACH else behavior
|
||||
if (
|
||||
previous_timer := pending_timers.pop(subscription_key, None)
|
||||
) is not None:
|
||||
previous_timer()
|
||||
|
||||
@callback
|
||||
def fire_after_duration(_now: datetime) -> None:
|
||||
"""Fire the action when the timer expires."""
|
||||
del pending_timers[subscription_key]
|
||||
call_action()
|
||||
|
||||
pending_timers[subscription_key] = async_call_later(
|
||||
self._hass, delay, fire_after_duration
|
||||
)
|
||||
|
||||
@callback
|
||||
def call_action() -> None:
|
||||
"""Call action with right context."""
|
||||
@@ -665,33 +689,21 @@ class EntityTriggerBase(Trigger):
|
||||
f"state of {entity_id}",
|
||||
event.context,
|
||||
)
|
||||
if self._repeat_interval:
|
||||
start_timer(self._repeat_interval)
|
||||
|
||||
if not self._duration:
|
||||
call_action()
|
||||
return
|
||||
|
||||
subscription_key = entity_id if behavior == BEHAVIOR_EACH else behavior
|
||||
if (
|
||||
previous_timer := pending_timers.pop(subscription_key, None)
|
||||
) is not None:
|
||||
previous_timer()
|
||||
|
||||
@callback
|
||||
def fire_after_duration(_now: datetime) -> None:
|
||||
"""Fire the action once the state has held for the duration."""
|
||||
del pending_timers[subscription_key]
|
||||
call_action()
|
||||
|
||||
pending_timers[subscription_key] = async_call_later(
|
||||
self._hass, self._duration, fire_after_duration
|
||||
)
|
||||
start_timer(self._duration)
|
||||
|
||||
unsub = await async_track_target_selector_state_change_event(
|
||||
self._hass,
|
||||
self._target,
|
||||
state_change_listener,
|
||||
self.entity_filter,
|
||||
handle_entities_update if self._duration else None,
|
||||
handle_entities_update if self._duration or self._repeat_interval else None,
|
||||
primary_entities_only=self._primary_entities_only,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user