Home Assistant Unofficial Reference 2024.12.1
device_trigger.py
Go to the documentation of this file.
1 """Provides device triggers for lutron caseta."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import cast
7 
8 import voluptuous as vol
9 
10 from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
11 from homeassistant.components.homeassistant.triggers import event as event_trigger
12 from homeassistant.const import (
13  CONF_DEVICE_ID,
14  CONF_DOMAIN,
15  CONF_EVENT,
16  CONF_PLATFORM,
17  CONF_TYPE,
18 )
19 from homeassistant.core import CALLBACK_TYPE, HomeAssistant
20 from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
21 from homeassistant.helpers.typing import ConfigType
22 
23 from .const import (
24  ACTION_PRESS,
25  ACTION_RELEASE,
26  ATTR_ACTION,
27  ATTR_BUTTON_TYPE,
28  CONF_SUBTYPE,
29  DOMAIN,
30  LUTRON_CASETA_BUTTON_EVENT,
31 )
32 from .models import LutronCasetaConfigEntry
33 
34 _LOGGER = logging.getLogger(__name__)
35 
36 
37 def _reverse_dict(forward_dict: dict) -> dict:
38  """Reverse a dictionary."""
39  return {v: k for k, v in forward_dict.items()}
40 
41 
42 SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE]
43 
44 LUTRON_BUTTON_TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
45  {
46  vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES),
47  }
48 )
49 
50 
51 KEYPAD_LEAP_BUTTON_NAME_OVERRIDE = {
52  "RRD-W2RLD": {
53  17: "raise_1",
54  16: "lower_1",
55  19: "raise_2",
56  18: "lower_2",
57  },
58  "RRD-W1RLD": {
59  19: "raise",
60  18: "lower",
61  },
62 }
63 
64 
65 PICO_2_BUTTON_BUTTON_TYPES_TO_LIP = {
66  "on": 2,
67  "off": 4,
68 }
69 PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP = {
70  "on": 0,
71  "off": 2,
72 }
73 PICO_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
74  {
75  vol.Required(CONF_SUBTYPE): vol.In(PICO_2_BUTTON_BUTTON_TYPES_TO_LIP),
76  }
77 )
78 
79 
80 PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP = {
81  "on": 2,
82  "off": 4,
83  "raise": 5,
84  "lower": 6,
85 }
86 PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = {
87  "on": 0,
88  "off": 2,
89  "raise": 3,
90  "lower": 4,
91 }
92 PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
93  {
94  vol.Required(CONF_SUBTYPE): vol.In(
95  PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP
96  ),
97  }
98 )
99 
100 
101 PICO_3_BUTTON_BUTTON_TYPES_TO_LIP = {
102  "on": 2,
103  "stop": 3,
104  "off": 4,
105 }
106 PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP = {
107  "on": 0,
108  "stop": 1,
109  "off": 2,
110 }
111 PICO_3_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
112  {
113  vol.Required(CONF_SUBTYPE): vol.In(PICO_3_BUTTON_BUTTON_TYPES_TO_LIP),
114  }
115 )
116 
117 PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP = {
118  "on": 2,
119  "stop": 3,
120  "off": 4,
121  "raise": 5,
122  "lower": 6,
123 }
124 PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP = {
125  "on": 0,
126  "stop": 1,
127  "off": 2,
128  "raise": 3,
129  "lower": 4,
130 }
131 PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
132  {
133  vol.Required(CONF_SUBTYPE): vol.In(
134  PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP
135  ),
136  }
137 )
138 
139 PICO_4_BUTTON_BUTTON_TYPES_TO_LIP = {
140  "button_1": 8,
141  "button_2": 9,
142  "button_3": 10,
143  "button_4": 11,
144 }
145 PICO_4_BUTTON_BUTTON_TYPES_TO_LEAP = {
146  "button_1": 1,
147  "button_2": 2,
148  "button_3": 3,
149  "button_4": 4,
150 }
151 LEAP_TO_PICO_4_BUTTON_BUTTON_TYPES = {
152  v: k for k, v in PICO_4_BUTTON_BUTTON_TYPES_TO_LEAP.items()
153 }
154 PICO_4_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
155  {
156  vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_BUTTON_TYPES_TO_LIP),
157  }
158 )
159 
160 
161 PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LIP = {
162  "on": 8,
163  "raise": 9,
164  "lower": 10,
165  "off": 11,
166 }
167 PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP = {
168  "on": 1,
169  "raise": 2,
170  "lower": 3,
171  "off": 4,
172 }
173 LEAP_TO_PICO_4_BUTTON_ZONE_BUTTON_TYPES = {
174  v: k for k, v in PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP.items()
175 }
176 PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
177  {
178  vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LIP),
179  }
180 )
181 
182 
183 PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LIP = {
184  "button_1": 8,
185  "button_2": 9,
186  "button_3": 10,
187  "off": 11,
188 }
189 PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP = {
190  "button_1": 1,
191  "button_2": 2,
192  "button_3": 3,
193  "off": 4,
194 }
195 PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
196  {
197  vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LIP),
198  }
199 )
200 
201 
202 PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP = {
203  "group_1_button_1": 8,
204  "group_1_button_2": 9,
205  "group_2_button_1": 10,
206  "group_2_button_2": 11,
207 }
208 PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP = {
209  "group_1_button_1": 1,
210  "group_1_button_2": 2,
211  "group_2_button_1": 3,
212  "group_2_button_2": 4,
213 }
214 PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
215  {
216  vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP),
217  }
218 )
219 
220 FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP = {
221  "open_all": 2,
222  "stop_all": 3,
223  "close_all": 4,
224  "raise_all": 5,
225  "lower_all": 6,
226  "open_1": 10,
227  "stop_1": 11,
228  "close_1": 12,
229  "raise_1": 13,
230  "lower_1": 14,
231  "open_2": 18,
232  "stop_2": 19,
233  "close_2": 20,
234  "raise_2": 21,
235  "lower_2": 22,
236  "open_3": 26,
237  "stop_3": 27,
238  "close_3": 28,
239  "raise_3": 29,
240  "lower_3": 30,
241  "open_4": 34,
242  "stop_4": 35,
243  "close_4": 36,
244  "raise_4": 37,
245  "lower_4": 38,
246 }
247 FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP = {
248  "open_all": 0,
249  "stop_all": 1,
250  "close_all": 2,
251  "raise_all": 3,
252  "lower_all": 4,
253  "open_1": 5,
254  "stop_1": 6,
255  "close_1": 7,
256  "raise_1": 8,
257  "lower_1": 9,
258  "open_2": 10,
259  "stop_2": 11,
260  "close_2": 12,
261  "raise_2": 13,
262  "lower_2": 14,
263  "open_3": 15,
264  "stop_3": 16,
265  "close_3": 17,
266  "raise_3": 18,
267  "lower_3": 19,
268  "open_4": 20,
269  "stop_4": 21,
270  "close_4": 22,
271  "raise_4": 23,
272  "lower_4": 24,
273 }
274 FOUR_GROUP_REMOTE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
275  {
276  vol.Required(CONF_SUBTYPE): vol.In(FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP),
277  }
278 )
279 
280 
281 DEVICE_TYPE_SCHEMA_MAP = {
282  "Pico2Button": PICO_2_BUTTON_TRIGGER_SCHEMA,
283  "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA,
284  "Pico3Button": PICO_3_BUTTON_TRIGGER_SCHEMA,
285  "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA,
286  "Pico4Button": PICO_4_BUTTON_TRIGGER_SCHEMA,
287  "Pico4ButtonScene": PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA,
288  "Pico4ButtonZone": PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA,
289  "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA,
290  "FourGroupRemote": FOUR_GROUP_REMOTE_TRIGGER_SCHEMA,
291 }
292 
293 DEVICE_TYPE_SUBTYPE_MAP_TO_LIP = {
294  "Pico2Button": PICO_2_BUTTON_BUTTON_TYPES_TO_LIP,
295  "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP,
296  "Pico3Button": PICO_3_BUTTON_BUTTON_TYPES_TO_LIP,
297  "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LIP,
298  "Pico4Button": PICO_4_BUTTON_BUTTON_TYPES_TO_LIP,
299  "Pico4ButtonScene": PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LIP,
300  "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LIP,
301  "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LIP,
302  "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LIP,
303 }
304 
305 DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP = {
306  "Pico2Button": PICO_2_BUTTON_BUTTON_TYPES_TO_LEAP,
307  "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP,
308  "Pico3Button": PICO_3_BUTTON_BUTTON_TYPES_TO_LEAP,
309  "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES_TO_LEAP,
310  "Pico4Button": PICO_4_BUTTON_BUTTON_TYPES_TO_LEAP,
311  "Pico4ButtonScene": PICO_4_BUTTON_SCENE_BUTTON_TYPES_TO_LEAP,
312  "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES_TO_LEAP,
313  "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES_TO_LEAP,
314  "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES_TO_LEAP,
315 }
316 
317 LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP: dict[str, dict[int, str]] = {
318  k: _reverse_dict(v) for k, v in DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.items()
319 }
320 
321 TRIGGER_SCHEMA = vol.Any(
322  PICO_2_BUTTON_TRIGGER_SCHEMA,
323  PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA,
324  PICO_4_BUTTON_TRIGGER_SCHEMA,
325  PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA,
326  PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA,
327  PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA,
328  FOUR_GROUP_REMOTE_TRIGGER_SCHEMA,
329 )
330 
331 
333  hass: HomeAssistant, config: ConfigType
334 ) -> ConfigType:
335  """Validate trigger config."""
336 
337  device_id = config[CONF_DEVICE_ID]
338  subtype = config[CONF_SUBTYPE]
339 
340  if not (data := get_lutron_data_by_dr_id(hass, device_id)) or not (
341  keypad := data.keypad_data.dr_device_id_to_keypad.get(device_id)
342  ):
343  return config
344 
345  keypad_trigger_schemas = data.keypad_data.trigger_schemas
346  keypad_button_names_to_leap = data.keypad_data.button_names_to_leap
347 
348  # Retrieve trigger schema, preferring hard-coded triggers from device_trigger.py
349  if not (
350  schema := DEVICE_TYPE_SCHEMA_MAP.get(
351  keypad["type"],
352  keypad_trigger_schemas.get(keypad["lutron_device_id"]),
353  )
354  ):
355  # Trigger schema not found - log error
356  _LOGGER.error(
357  "Cannot validate trigger %s because the trigger schema was not found",
358  config,
359  )
360  return config
361 
362  # Retrieve list of valid buttons, preferring hard-coded triggers from device_trigger.py
363  device_type = keypad["type"]
364  valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(
365  device_type,
366  keypad_button_names_to_leap[keypad["lutron_device_id"]],
367  )
368 
369  if subtype not in valid_buttons:
370  # Trigger subtype is invalid - raise error
371  _LOGGER.error(
372  "Cannot validate trigger %s because subtype %s is invalid", config, subtype
373  )
374  return config
375 
376  return schema(config)
377 
378 
380  hass: HomeAssistant, device_id: str
381 ) -> list[dict[str, str]]:
382  """List device triggers for lutron caseta devices."""
383  # Check if device is a valid keypad. Return empty if not.
384  if not (data := get_lutron_data_by_dr_id(hass, device_id)) or not (
385  keypad := data.keypad_data.dr_device_id_to_keypad.get(device_id)
386  ):
387  return []
388 
389  keypad_button_names_to_leap = data.keypad_data.button_names_to_leap
390 
391  # Retrieve list of valid buttons, preferring hard-coded triggers from device_trigger.py
392  valid_buttons = DEVICE_TYPE_SUBTYPE_MAP_TO_LEAP.get(
393  keypad["type"],
394  keypad_button_names_to_leap[keypad["lutron_device_id"]],
395  )
396 
397  return [
398  {
399  CONF_PLATFORM: "device",
400  CONF_DEVICE_ID: device_id,
401  CONF_DOMAIN: DOMAIN,
402  CONF_TYPE: trigger,
403  CONF_SUBTYPE: subtype,
404  }
405  for trigger in SUPPORTED_INPUTS_EVENTS_TYPES
406  for subtype in valid_buttons
407  ]
408 
409 
411  hass: HomeAssistant,
412  config: ConfigType,
413  action: TriggerActionType,
414  trigger_info: TriggerInfo,
415 ) -> CALLBACK_TYPE:
416  """Attach a trigger."""
417  return await event_trigger.async_attach_trigger(
418  hass,
419  event_trigger.TRIGGER_SCHEMA(
420  {
421  event_trigger.CONF_PLATFORM: CONF_EVENT,
422  event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT,
423  event_trigger.CONF_EVENT_DATA: {
424  CONF_DEVICE_ID: config[CONF_DEVICE_ID],
425  ATTR_ACTION: config[CONF_TYPE],
426  ATTR_BUTTON_TYPE: config[CONF_SUBTYPE],
427  },
428  }
429  ),
430  action,
431  trigger_info,
432  platform_type="device",
433  )
434 
435 
436 def get_lutron_data_by_dr_id(hass: HomeAssistant, device_id: str):
437  """Get a lutron integration data for the given device registry device id."""
438  entries = cast(
439  list[LutronCasetaConfigEntry],
440  hass.config_entries.async_entries(
441  DOMAIN, include_ignore=False, include_disabled=False
442  ),
443  )
444  for entry in entries:
445  if hasattr(entry, "runtime_data"):
446  if entry.runtime_data.keypad_data.dr_device_id_to_keypad.get(device_id):
447  return entry.runtime_data
448  return None
list[dict[str, str]] async_get_triggers(HomeAssistant hass, str device_id)
ConfigType async_validate_trigger_config(HomeAssistant hass, ConfigType config)
def get_lutron_data_by_dr_id(HomeAssistant hass, str device_id)
CALLBACK_TYPE async_attach_trigger(HomeAssistant hass, ConfigType config, TriggerActionType action, TriggerInfo trigger_info)