1 """Offer state listening automation rules."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from datetime
import timedelta
9 import voluptuous
as vol
11 from homeassistant
import exceptions
16 EventStateChangedData,
23 config_validation
as cv,
24 entity_registry
as er,
28 async_track_same_state,
29 async_track_state_change_event,
35 _LOGGER = logging.getLogger(__name__)
37 CONF_ENTITY_ID =
"entity_id"
40 CONF_NOT_FROM =
"not_from"
41 CONF_NOT_TO =
"not_to"
43 BASE_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
45 vol.Required(CONF_PLATFORM):
"state",
46 vol.Required(CONF_ENTITY_ID): cv.entity_ids_or_uuids,
47 vol.Optional(CONF_FOR): cv.positive_time_period_template,
48 vol.Optional(CONF_ATTRIBUTE): cv.match_all,
52 TRIGGER_STATE_SCHEMA = BASE_SCHEMA.extend(
55 vol.Exclusive(CONF_FROM, CONF_FROM): vol.Any(str, [str],
None),
56 vol.Exclusive(CONF_NOT_FROM, CONF_FROM): vol.Any(str, [str],
None),
57 vol.Exclusive(CONF_TO, CONF_TO): vol.Any(str, [str],
None),
58 vol.Exclusive(CONF_NOT_TO, CONF_TO): vol.Any(str, [str],
None),
62 TRIGGER_ATTRIBUTE_SCHEMA = BASE_SCHEMA.extend(
64 vol.Exclusive(CONF_FROM, CONF_FROM): cv.match_all,
65 vol.Exclusive(CONF_NOT_FROM, CONF_FROM): cv.match_all,
66 vol.Exclusive(CONF_TO, CONF_TO): cv.match_all,
67 vol.Exclusive(CONF_NOT_TO, CONF_TO): cv.match_all,
73 hass: HomeAssistant, config: ConfigType
75 """Validate trigger config."""
76 if not isinstance(config, dict):
77 raise vol.Invalid(
"Expected a dictionary")
81 if CONF_ATTRIBUTE
in config:
86 registry = er.async_get(hass)
87 config[CONF_ENTITY_ID] = er.async_validate_entity_ids(
88 registry, cv.entity_ids_or_uuids(config[CONF_ENTITY_ID])
97 action: TriggerActionType,
98 trigger_info: TriggerInfo,
100 platform_type: str =
"state",
102 """Listen for state changes based on configuration."""
103 entity_ids = config[CONF_ENTITY_ID]
105 if (from_state := config.get(CONF_FROM))
is not None:
107 elif (not_from_state := config.get(CONF_NOT_FROM))
is not None:
112 if (to_state := config.get(CONF_TO))
is not None:
114 elif (not_to_state := config.get(CONF_NOT_TO))
is not None:
119 time_delta = config.get(CONF_FOR)
123 item
not in config
for item
in (CONF_FROM, CONF_NOT_FROM, CONF_NOT_TO, CONF_TO)
125 unsub_track_same: dict[str, Callable[[],
None]] = {}
126 period: dict[str, timedelta] = {}
127 attribute = config.get(CONF_ATTRIBUTE)
128 job =
HassJob(action, f
"state trigger {trigger_info}")
130 trigger_data = trigger_info[
"trigger_data"]
131 _variables = trigger_info[
"variables"]
or {}
134 def state_automation_listener(event: Event[EventStateChangedData]) ->
None:
135 """Listen for state changes and calls action."""
136 entity = event.data[
"entity_id"]
137 from_s = event.data[
"old_state"]
138 to_s = event.data[
"new_state"]
142 elif attribute
is None:
143 old_value = from_s.state
145 old_value = from_s.attributes.get(attribute)
149 elif attribute
is None:
150 new_value = to_s.state
152 new_value = to_s.attributes.get(attribute)
158 if attribute
is not None and old_value == new_value:
162 not match_from_state(old_value)
163 or not match_to_state(new_value)
164 or (
not match_all
and old_value == new_value)
169 def call_action() -> None:
170 """Call action with right context."""
171 hass.async_run_hass_job(
176 "platform": platform_type,
178 "from_state": from_s,
180 "for": time_delta
if not time_delta
else period[entity],
181 "attribute": attribute,
182 "description": f
"state of {entity}",
196 "from_state": from_s,
200 variables = {**_variables, **data}
203 period[entity] = cv.positive_time_period(
204 template.render_complex(time_delta, variables)
208 "Error rendering '%s' for template: %s", trigger_info[
"name"], ex
212 def _check_same_state(_: str, _2: State |
None, new_st: State |
None) -> bool:
216 cur_value: str |
None
217 if attribute
is None:
218 cur_value = new_st.state
220 cur_value = new_st.attributes.get(attribute)
222 if CONF_FROM
in config
and CONF_TO
not in config:
223 return cur_value != old_value
225 return cur_value == new_value
239 """Remove state listeners async."""
241 for async_remove
in unsub_track_same.values():
243 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="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)
Callable[[str|None], bool] process_state_match(str|Iterable[str]|None parameter, bool invert=False)
None async_remove(HomeAssistant hass, str intent_type)