1 """Offer numeric state listening automation rules."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from datetime
import timedelta
10 import voluptuous
as vol
12 from homeassistant
import exceptions
25 EventStateChangedData,
33 config_validation
as cv,
34 entity_registry
as er,
38 async_track_same_state,
39 async_track_state_change_event,
45 def validate_above_below[_T: dict[str, Any]](value: _T) -> _T:
46 """Validate that above and below can co-exist."""
47 above = value.get(CONF_ABOVE)
48 below = value.get(CONF_BELOW)
50 if above
is None or below
is None:
53 if isinstance(above, str)
or isinstance(below, str):
59 f
"A value can never be above {above} and below {below} at the same"
60 " time. You probably want two different triggers."
67 _TRIGGER_SCHEMA = vol.All(
68 cv.TRIGGER_BASE_SCHEMA.extend(
70 vol.Required(CONF_PLATFORM):
"numeric_state",
71 vol.Required(CONF_ENTITY_ID): cv.entity_ids_or_uuids,
72 vol.Optional(CONF_BELOW): cv.NUMERIC_STATE_THRESHOLD_SCHEMA,
73 vol.Optional(CONF_ABOVE): cv.NUMERIC_STATE_THRESHOLD_SCHEMA,
74 vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
75 vol.Optional(CONF_FOR): cv.positive_time_period_template,
76 vol.Optional(CONF_ATTRIBUTE): cv.match_all,
79 cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE),
83 _LOGGER = logging.getLogger(__name__)
87 hass: HomeAssistant, config: ConfigType
89 """Validate trigger config."""
91 registry = er.async_get(hass)
92 config[CONF_ENTITY_ID] = er.async_validate_entity_ids(
93 registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID])
101 action: TriggerActionType,
102 trigger_info: TriggerInfo,
104 platform_type: str =
"numeric_state",
106 """Listen for state changes based on configuration."""
107 entity_ids: list[str] = config[CONF_ENTITY_ID]
108 below = config.get(CONF_BELOW)
109 above = config.get(CONF_ABOVE)
110 time_delta = config.get(CONF_FOR)
111 value_template = config.get(CONF_VALUE_TEMPLATE)
112 unsub_track_same: dict[str, Callable[[],
None]] = {}
113 armed_entities: set[str] = set()
114 period: dict[str, timedelta] = {}
115 attribute = config.get(CONF_ATTRIBUTE)
116 job =
HassJob(action, f
"numeric state trigger {trigger_info}")
118 trigger_data = trigger_info[
"trigger_data"]
119 _variables = trigger_info[
"variables"]
or {}
121 def variables(entity_id: str) -> dict[str, Any]:
122 """Return a dict with trigger variables."""
125 "platform":
"numeric_state",
126 "entity_id": entity_id,
129 "attribute": attribute,
132 return {**_variables, **trigger_info}
135 def check_numeric_state(
136 entity_id: str, from_s: State |
None, to_s: str | State |
None
138 """Return whether the criteria are met, raise ConditionError if unknown."""
139 return condition.async_numeric_state(
140 hass, to_s, below, above, value_template, variables(entity_id), attribute
144 for entity_id
in entity_ids:
146 if not check_numeric_state(entity_id,
None, entity_id):
147 armed_entities.add(entity_id)
150 "Error initializing '%s' trigger: %s",
151 trigger_info[
"name"],
156 def state_automation_listener(event: Event[EventStateChangedData]) ->
None:
157 """Listen for state changes and calls action."""
158 entity_id = event.data[
"entity_id"]
159 from_s = event.data[
"old_state"]
160 to_s = event.data[
"new_state"]
166 def call_action() -> None:
167 """Call action with right context."""
168 hass.async_run_hass_job(
173 "platform": platform_type,
174 "entity_id": entity_id,
177 "from_state": from_s,
179 "for": time_delta
if not time_delta
else period[entity_id],
180 "description": f
"numeric state of {entity_id}",
187 def check_numeric_state_no_raise(
188 entity_id: str, from_s: State |
None, to_s: State |
None
190 """Return True if the criteria are now met, False otherwise."""
192 return check_numeric_state(entity_id, from_s, to_s)
200 matching = check_numeric_state(entity_id, from_s, to_s)
202 _LOGGER.warning(
"Error in '%s' trigger: %s", trigger_info[
"name"], ex)
206 armed_entities.add(entity_id)
207 elif entity_id
in armed_entities:
208 armed_entities.discard(entity_id)
212 period[entity_id] = cv.positive_time_period(
213 template.render_complex(time_delta, variables(entity_id))
217 "Error rendering '%s' for template: %s",
218 trigger_info[
"name"],
227 entity_ids=entity_id,
228 async_check_same_func=check_numeric_state_no_raise,
237 """Remove state listeners async."""
239 for async_remove
in unsub_track_same.values():
241 unsub_track_same.clear()
ConfigType async_validate_trigger_config(HomeAssistant hass, ConfigType config)
CALLBACK_TYPE async_attach_trigger(HomeAssistant hass, ConfigType config, TriggerActionType action, TriggerInfo trigger_info, *str platform_type="numeric_state")
CALLBACK_TYPE async_track_same_state(HomeAssistant hass, timedelta period, Callable[[], Coroutine[Any, Any, None]|None] action, Callable[[str, State|None, State|None], bool] async_check_same_func, str|Iterable[str] entity_ids=MATCH_ALL)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
None async_remove(HomeAssistant hass, str intent_type)