Home Assistant Unofficial Reference 2024.12.1
trigger.py
Go to the documentation of this file.
1 """Offer geolocation automation rules."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Final
7 
8 import voluptuous as vol
9 
10 from homeassistant.const import CONF_EVENT, CONF_PLATFORM, CONF_SOURCE, CONF_ZONE
11 from homeassistant.core import (
12  CALLBACK_TYPE,
13  Event,
14  EventStateChangedData,
15  HassJob,
16  HomeAssistant,
17  State,
18  callback,
19 )
20 from homeassistant.helpers import condition, config_validation as cv
21 from homeassistant.helpers.config_validation import entity_domain
22 from homeassistant.helpers.event import TrackStates, async_track_state_change_filtered
23 from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
24 from homeassistant.helpers.typing import ConfigType
25 
26 from . import DOMAIN
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 EVENT_ENTER: Final = "enter"
31 EVENT_LEAVE: Final = "leave"
32 DEFAULT_EVENT: Final = EVENT_ENTER
33 
34 TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
35  {
36  vol.Required(CONF_PLATFORM): "geo_location",
37  vol.Required(CONF_SOURCE): cv.string,
38  vol.Required(CONF_ZONE): entity_domain("zone"),
39  vol.Required(CONF_EVENT, default=DEFAULT_EVENT): vol.Any(
40  EVENT_ENTER, EVENT_LEAVE
41  ),
42  }
43 )
44 
45 
46 def source_match(state: State | None, source: str) -> bool:
47  """Check if the state matches the provided source."""
48  return state is not None and state.attributes.get("source") == source
49 
50 
52  hass: HomeAssistant,
53  config: ConfigType,
54  action: TriggerActionType,
55  trigger_info: TriggerInfo,
56 ) -> CALLBACK_TYPE:
57  """Listen for state changes based on configuration."""
58  trigger_data = trigger_info["trigger_data"]
59  source: str = config[CONF_SOURCE].lower()
60  zone_entity_id: str = config[CONF_ZONE]
61  trigger_event: str = config[CONF_EVENT]
62  job = HassJob(action)
63 
64  @callback
65  def state_change_listener(event: Event[EventStateChangedData]) -> None:
66  """Handle specific state changes."""
67  # Skip if the event's source does not match the trigger's source.
68  from_state = event.data["old_state"]
69  to_state = event.data["new_state"]
70  if not source_match(from_state, source) and not source_match(to_state, source):
71  return
72 
73  if (zone_state := hass.states.get(zone_entity_id)) is None:
74  _LOGGER.warning(
75  "Unable to execute automation %s: Zone %s not found",
76  trigger_info["name"],
77  zone_entity_id,
78  )
79  return
80 
81  from_match = (
82  condition.zone(hass, zone_state, from_state) if from_state else False
83  )
84  to_match = condition.zone(hass, zone_state, to_state) if to_state else False
85 
86  if (
87  trigger_event == EVENT_ENTER
88  and not from_match
89  and to_match
90  or trigger_event == EVENT_LEAVE
91  and from_match
92  and not to_match
93  ):
94  hass.async_run_hass_job(
95  job,
96  {
97  "trigger": {
98  **trigger_data,
99  "platform": "geo_location",
100  "source": source,
101  "entity_id": event.data["entity_id"],
102  "from_state": from_state,
103  "to_state": to_state,
104  "zone": zone_state,
105  "event": trigger_event,
106  "description": f"geo_location - {source}",
107  }
108  },
109  event.context,
110  )
111 
113  hass, TrackStates(False, set(), {DOMAIN}), state_change_listener
114  ).async_remove
bool source_match(State|None state, str source)
Definition: trigger.py:46
CALLBACK_TYPE async_attach_trigger(HomeAssistant hass, ConfigType config, TriggerActionType action, TriggerInfo trigger_info)
Definition: trigger.py:56
Callable[[Any], str] entity_domain(str|list[str] domain)
_TrackStateChangeFiltered async_track_state_change_filtered(HomeAssistant hass, TrackStates track_states, Callable[[Event[EventStateChangedData]], Any] action)
Definition: event.py:861