1 """Provides device automations for homekit devices."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Generator
6 from typing
import TYPE_CHECKING, Any
8 from aiohomekit.model.characteristics
import CharacteristicsTypes
9 from aiohomekit.model.characteristics.const
import InputEventValues
10 from aiohomekit.model.services
import Service, ServicesTypes
11 from aiohomekit.utils
import clamp_enum_to_char
12 import voluptuous
as vol
21 from .const
import DOMAIN, KNOWN_DEVICES, TRIGGERS
24 from .connection
import HKDevice
39 TRIGGER_SUBTYPES = {
"single_press",
"double_press",
"long_press"}
42 CONF_SUBTYPE =
"subtype"
44 TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
46 vol.Required(CONF_TYPE): vol.In(TRIGGER_TYPES),
47 vol.Required(CONF_SUBTYPE): vol.In(TRIGGER_SUBTYPES),
51 HK_TO_HA_INPUT_EVENT_VALUES = {
52 InputEventValues.SINGLE_PRESS:
"single_press",
53 InputEventValues.DOUBLE_PRESS:
"double_press",
54 InputEventValues.LONG_PRESS:
"long_press",
59 """Represents a stateless source of event data from HomeKit."""
61 def __init__(self, hass: HomeAssistant) ->
None:
62 """Initialize a set of triggers for a device."""
64 self._triggers: dict[tuple[str, str], dict[str, Any]] = {}
65 self._callbacks: dict[tuple[str, str], list[Callable[[Any],
None]]] = {}
66 self._iid_trigger_keys: dict[int, set[tuple[str, str]]] = {}
70 self, connection: HKDevice, aid: int, triggers: list[dict[str, Any]]
72 """Set up a set of triggers for a device.
74 This function must be re-entrant since
75 it is called when the device is first added and
76 when the config entry is reloaded.
78 for trigger_data
in triggers:
79 trigger_key = (trigger_data[CONF_TYPE], trigger_data[CONF_SUBTYPE])
80 self._triggers[trigger_key] = trigger_data
81 iid = trigger_data[
"characteristic"]
82 self._iid_trigger_keys.setdefault(iid, set()).
add(trigger_key)
83 connection.add_watchable_characteristics([(aid, iid)])
85 def fire(self, iid: int, ev: dict[str, Any]) ->
None:
86 """Process events that have been received from a HomeKit accessory."""
87 for trigger_key
in self._iid_trigger_keys.
get(iid, set()):
88 for event_handler
in self._callbacks.
get(trigger_key, []):
92 """List device triggers for HomeKit devices."""
93 yield from self._triggers
99 action: TriggerActionType,
100 trigger_info: TriggerInfo,
102 """Attach a trigger."""
103 trigger_data = trigger_info[
"trigger_data"]
104 type_: str = config[CONF_TYPE]
105 sub_type: str = config[CONF_SUBTYPE]
106 trigger_key = (type_, sub_type)
108 trigger_callbacks = self._callbacks.setdefault(trigger_key, [])
109 hass = self.
_hass_hass
112 def event_handler(ev: dict[str, Any]) ->
None:
113 if sub_type != HK_TO_HA_INPUT_EVENT_VALUES[ev[
"value"]]:
115 hass.async_run_hass_job(job, {
"trigger": {**trigger_data, **config}})
117 trigger_callbacks.append(event_handler)
119 def async_remove_handler() -> None:
120 trigger_callbacks.remove(event_handler)
122 return async_remove_handler
126 """Enumerate a stateless switch, like a single button."""
131 service.has(CharacteristicsTypes.SERVICE_LABEL_INDEX)
132 and len(service.linked) > 0
136 char = service[CharacteristicsTypes.INPUT_EVENT]
140 all_values = clamp_enum_to_char(InputEventValues, char)
144 "characteristic": char.iid,
147 "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type],
149 for event_type
in all_values
154 """Enumerate a group of stateless switches, like a remote control."""
156 service.accessory.services.filter(
157 service_type=ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH,
158 child_service=service,
159 order_by=[CharacteristicsTypes.SERVICE_LABEL_INDEX],
163 results: list[dict[str, Any]] = []
164 for idx, switch
in enumerate(switches):
165 char = switch[CharacteristicsTypes.INPUT_EVENT]
169 all_values = clamp_enum_to_char(InputEventValues, char)
173 "characteristic": char.iid,
175 "type": f
"button{idx + 1}",
176 "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type],
178 for event_type
in all_values
184 """Enumerate doorbell buttons."""
185 input_event = service[CharacteristicsTypes.INPUT_EVENT]
189 all_values = clamp_enum_to_char(InputEventValues, input_event)
193 "characteristic": input_event.iid,
196 "subtype": HK_TO_HA_INPUT_EVENT_VALUES[event_type],
198 for event_type
in all_values
203 ServicesTypes.SERVICE_LABEL: enumerate_stateless_switch_group,
204 ServicesTypes.STATELESS_PROGRAMMABLE_SWITCH: enumerate_stateless_switch,
205 ServicesTypes.DOORBELL: enumerate_doorbell,
210 hass: HomeAssistant, config_entry: ConfigEntry
212 """Triggers aren't entities as they have no state, but we still need to set them up for a config entry."""
213 hkid = config_entry.data[
"AccessoryPairingID"]
214 conn: HKDevice = hass.data[KNOWN_DEVICES][hkid]
218 aid = service.accessory.aid
219 service_type = service.type
222 if service_type
not in TRIGGER_FINDERS:
229 device_id = conn.devices[aid]
230 if TRIGGERS
in hass.data
and device_id
in hass.data[TRIGGERS]:
235 triggers = TRIGGER_FINDERS[service_type](service)
236 if len(triggers) == 0:
240 trigger.async_setup(conn, aid, triggers)
244 conn.add_trigger_factory(async_add_characteristic)
249 hass: HomeAssistant, device_id: str
251 """Get or create a trigger source for a device id."""
252 trigger_sources: dict[str, TriggerSource] = hass.data.setdefault(TRIGGERS, {})
253 if not (source := trigger_sources.get(device_id)):
255 trigger_sources[device_id] = source
260 conn: HKDevice, events: dict[tuple[int, int], dict[str, Any]]
262 """Process events generated by a HomeKit accessory into automation triggers."""
263 trigger_sources: dict[str, TriggerSource] = conn.hass.data.get(TRIGGERS, {})
264 if not trigger_sources:
266 for (aid, iid), ev
in events.items():
267 if aid
in conn.devices:
268 device_id = conn.devices[aid]
269 if source := trigger_sources.get(device_id):
272 if ev.get(
"value")
is not None:
277 hass: HomeAssistant, device_id: str
278 ) -> list[dict[str, str]]:
279 """List device triggers for homekit devices."""
281 if device_id
not in hass.data.get(TRIGGERS, {}):
284 device: TriggerSource = hass.data[TRIGGERS][device_id]
288 CONF_PLATFORM:
"device",
289 CONF_DEVICE_ID: device_id,
292 CONF_SUBTYPE: subtype,
294 for trigger, subtype
in device.async_get_triggers()
301 action: TriggerActionType,
302 trigger_info: TriggerInfo,
304 """Attach a trigger."""
305 device_id = config[CONF_DEVICE_ID]
307 config, action, trigger_info
None fire(self, int iid, dict[str, Any] ev)
Generator[tuple[str, str]] async_get_triggers(self)
None async_setup(self, HKDevice connection, int aid, list[dict[str, Any]] triggers)
None __init__(self, HomeAssistant hass)
CALLBACK_TYPE async_attach_trigger(self, ConfigType config, TriggerActionType action, TriggerInfo trigger_info)
bool add(self, _T matcher)
web.Response get(self, web.Request request, str config_key)
list[dict[str, Any]] enumerate_doorbell(Service service)
CALLBACK_TYPE async_attach_trigger(HomeAssistant hass, ConfigType config, TriggerActionType action, TriggerInfo trigger_info)
list[dict[str, Any]] enumerate_stateless_switch_group(Service service)
list[dict[str, Any]] enumerate_stateless_switch(Service service)
list[dict[str, str]] async_get_triggers(HomeAssistant hass, str device_id)
None async_setup_triggers_for_entry(HomeAssistant hass, ConfigEntry config_entry)
None async_fire_triggers(HKDevice conn, dict[tuple[int, int], dict[str, Any]] events)
TriggerSource async_get_or_create_trigger_source(HomeAssistant hass, str device_id)
bool async_add_characteristic(Characteristic char)