1 """Component for interacting with a Lutron Caseta system."""
3 from __future__
import annotations
7 from itertools
import chain
10 from typing
import Any, cast
12 from pylutron_caseta
import BUTTON_STATUS_PRESSED
13 from pylutron_caseta.smartbridge
import Smartbridge
14 import voluptuous
as vol
16 from homeassistant
import config_entries
33 ATTR_LEAP_BUTTON_NUMBER,
43 LUTRON_CASETA_BUTTON_EVENT,
47 from .device_trigger
import (
48 DEVICE_TYPE_SUBTYPE_MAP_TO_LIP,
49 KEYPAD_LEAP_BUTTON_NAME_OVERRIDE,
50 LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP,
51 LUTRON_BUTTON_TRIGGER_SCHEMA,
54 LUTRON_BUTTON_LEAP_BUTTON_NUMBER,
55 LUTRON_KEYPAD_AREA_NAME,
56 LUTRON_KEYPAD_BUTTONS,
57 LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID,
58 LUTRON_KEYPAD_LUTRON_DEVICE_ID,
64 LutronCasetaConfigEntry,
69 from .util
import area_name_from_id, serial_to_unique_id
71 _LOGGER = logging.getLogger(__name__)
73 DATA_BRIDGE_CONFIG =
"lutron_caseta_bridges"
75 CONFIG_SCHEMA = vol.Schema(
81 vol.Required(CONF_HOST): cv.string,
82 vol.Required(CONF_KEYFILE): cv.string,
83 vol.Required(CONF_CERTFILE): cv.string,
84 vol.Required(CONF_CA_CERTS): cv.string,
89 extra=vol.ALLOW_EXTRA,
93 Platform.BINARY_SENSOR,
103 async
def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
104 """Set up the Lutron component."""
105 if DOMAIN
in base_config:
106 bridge_configs = base_config[DOMAIN]
107 for config
in bridge_configs:
108 hass.async_create_task(
109 hass.config_entries.flow.async_init(
111 context={
"source": config_entries.SOURCE_IMPORT},
114 CONF_HOST: config[CONF_HOST],
115 CONF_KEYFILE: config[CONF_KEYFILE],
116 CONF_CERTFILE: config[CONF_CERTFILE],
117 CONF_CA_CERTS: config[CONF_CA_CERTS],
126 hass: HomeAssistant, entry: LutronCasetaConfigEntry
128 """Migrate entities since the occupancygroup were not actually unique."""
130 dev_reg = dr.async_get(hass)
131 bridge_unique_id = entry.unique_id
134 def _async_migrator(entity_entry: er.RegistryEntry) -> dict[str, Any] |
None:
135 if not (unique_id := entity_entry.unique_id):
137 if not unique_id.startswith(
"occupancygroup_")
or unique_id.startswith(
138 f
"occupancygroup_{bridge_unique_id}"
141 sensor_id = unique_id.split(
"_")[1]
142 new_unique_id = f
"occupancygroup_{bridge_unique_id}_{sensor_id}"
143 if dev_entry := dev_reg.async_get_device(identifiers={(DOMAIN, unique_id)}):
144 dev_reg.async_update_device(
145 dev_entry.id, new_identifiers={(DOMAIN, new_unique_id)}
147 return {
"new_unique_id": f
"occupancygroup_{bridge_unique_id}_{sensor_id}"}
149 await er.async_migrate_entries(hass, entry.entry_id, _async_migrator)
153 hass: HomeAssistant, entry: LutronCasetaConfigEntry
155 """Set up a bridge from a config entry."""
156 entry_id = entry.entry_id
157 host = entry.data[CONF_HOST]
158 keyfile = hass.config.path(entry.data[CONF_KEYFILE])
159 certfile = hass.config.path(entry.data[CONF_CERTFILE])
160 ca_certs = hass.config.path(entry.data[CONF_CA_CERTS])
164 bridge = Smartbridge.create_tls(
165 hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs
168 _LOGGER.error(
"Invalid certificate used to connect to bridge at %s", host)
172 with contextlib.suppress(TimeoutError):
173 async
with asyncio.timeout(BRIDGE_TIMEOUT):
174 await bridge.connect()
177 if timed_out
or not bridge.is_connected():
181 if not bridge.is_connected():
184 _LOGGER.debug(
"Connected to Lutron Caseta bridge via LEAP at %s", host)
187 bridge_devices = bridge.get_devices()
188 bridge_device = bridge_devices[BRIDGE_DEVICE_ID]
190 if not entry.unique_id:
191 hass.config_entries.async_update_entry(
204 await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
211 hass: HomeAssistant, config_entry_id: str, bridge_device: dict, bridge: Smartbridge
213 """Register the bridge device in the device registry."""
214 device_registry = dr.async_get(hass)
217 name=bridge_device[
"name"],
218 manufacturer=MANUFACTURER,
219 identifiers={(DOMAIN, bridge_device[
"serial"])},
220 model=f
"{bridge_device['model']} ({bridge_device['type']})",
221 via_device=(DOMAIN, bridge_device[
"serial"]),
222 configuration_url=
"https://device-login.lutron.com",
226 if area != UNASSIGNED_AREA:
227 device_args[
"suggested_area"] = area
229 device_registry.async_get_or_create(**device_args, config_entry_id=config_entry_id)
235 config_entry_id: str,
237 bridge_device: dict[str, str | int],
238 ) -> LutronKeypadData:
239 """Register keypad devices (Keypads and Pico Remotes) in the device registry."""
241 device_registry = dr.async_get(hass)
243 bridge_devices: dict[str, dict[str, str | int]] = bridge.get_devices()
244 bridge_buttons: dict[str, dict[str, str | int]] = bridge.buttons
246 dr_device_id_to_keypad: dict[str, LutronKeypad] = {}
247 keypads: dict[int, LutronKeypad] = {}
248 keypad_buttons: dict[int, LutronButton] = {}
249 keypad_button_names_to_leap: dict[int, dict[str, int]] = {}
250 leap_to_keypad_button_names: dict[int, dict[int, str]] = {}
252 for bridge_button
in bridge_buttons.values():
253 parent_device = cast(str, bridge_button[
"parent_device"])
254 bridge_keypad = bridge_devices[parent_device]
255 keypad_lutron_device_id = cast(int, bridge_keypad[
"device_id"])
256 button_lutron_device_id = cast(int, bridge_button[
"device_id"])
257 leap_button_number = cast(int, bridge_button[
"button_number"])
258 button_led_device_id =
None
259 if "button_led" in bridge_button:
260 button_led_device_id = cast(str, bridge_button[
"button_led"])
262 if not (keypad := keypads.get(keypad_lutron_device_id)):
265 bridge, bridge_device, bridge_keypad, keypad_lutron_device_id
269 dr_device = device_registry.async_get_or_create(
270 **keypad[
"device_info"], config_entry_id=config_entry_id
272 keypad[LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID] = dr_device.id
273 dr_device_id_to_keypad[dr_device.id] = keypad
276 keypad_lutron_device_id = keypad[LUTRON_KEYPAD_LUTRON_DEVICE_ID]
280 lutron_device_id=button_lutron_device_id,
281 leap_button_number=leap_button_number,
282 button_name=button_name,
283 led_device_id=button_led_device_id,
284 parent_keypad=keypad_lutron_device_id,
287 keypad[LUTRON_KEYPAD_BUTTONS].append(button_lutron_device_id)
289 button_name_to_leap = keypad_button_names_to_leap.setdefault(
290 keypad_lutron_device_id, {}
292 button_name_to_leap[button_name] = leap_button_number
293 leap_to_button_name = leap_to_keypad_button_names.setdefault(
294 keypad_lutron_device_id, {}
296 leap_to_button_name[leap_button_number] = button_name
304 keypad_buttons=keypad_buttons,
305 leap_to_keypad_button_names=leap_to_keypad_button_names,
309 dr_device_id_to_keypad,
312 keypad_button_names_to_leap,
313 keypad_trigger_schemas,
319 keypad_button_names_to_leap: dict[int, dict[str, int]],
320 ) -> dict[int, vol.Schema]:
321 """Build device trigger schemas."""
324 keypad_id: LUTRON_BUTTON_TRIGGER_SCHEMA.extend(
326 vol.Required(CONF_SUBTYPE): vol.In(
327 keypad_button_names_to_leap[keypad_id]
331 for keypad_id
in keypad_button_names_to_leap
338 bridge_device: dict[str, Any],
339 bridge_keypad: dict[str, Any],
340 keypad_device_id: int,
344 keypad_name = bridge_keypad[
"name"].split(
"_")[-1]
347 name=f
"{area_name} {keypad_name}",
348 manufacturer=MANUFACTURER,
349 identifiers={(DOMAIN, keypad_serial)},
350 model=f
"{bridge_keypad['model']} ({bridge_keypad['type']})",
351 via_device=(DOMAIN, bridge_device[
"serial"]),
353 if area_name != UNASSIGNED_AREA:
354 device_info[
"suggested_area"] = area_name
357 lutron_device_id=keypad_device_id,
359 area_id=bridge_keypad[
"area"],
362 serial=keypad_serial,
363 device_info=device_info,
364 model=bridge_keypad[
"model"],
365 type=bridge_keypad[
"type"],
371 """Get the LEAP button name and check for override."""
373 button_number = bridge_button[
"button_number"]
374 button_name = bridge_button.get(
"device_name")
376 if button_name
is None:
380 keypad_model = keypad[LUTRON_KEYPAD_MODEL]
381 if keypad_model_override := KEYPAD_LEAP_BUTTON_NAME_OVERRIDE.get(keypad_model):
382 if alt_button_name := keypad_model_override.get(button_number):
383 return alt_button_name
389 """Retrieve the caseta button name from device triggers."""
390 button_number_map = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get(keypad[
"type"], {})
392 button_number_map.get(
394 f
"button {button_number}",
402 return keypad_device[
"serial"]
or f
"{bridge_serial}_{keypad_device['device_id']}"
407 """Get the LIP button for a given LEAP button."""
409 lip_buttons_name_to_num := DEVICE_TYPE_SUBTYPE_MAP_TO_LIP.get(device_type)
411 leap_button_num_to_name := LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get(device_type)
414 return lip_buttons_name_to_num[leap_button_num_to_name[leap_button]]
421 keypads: dict[int, LutronKeypad],
422 keypad_buttons: dict[int, LutronButton],
423 leap_to_keypad_button_names: dict[int, dict[int, str]],
425 """Subscribe to lutron events."""
428 def _async_button_event(button_id, event_type):
429 if not (button := keypad_buttons.get(button_id))
or not (
430 keypad := keypads.get(button[
"parent_keypad"])
434 if event_type == BUTTON_STATUS_PRESSED:
435 action = ACTION_PRESS
437 action = ACTION_RELEASE
439 keypad_type = keypad[LUTRON_KEYPAD_TYPE]
440 keypad_device_id = keypad[LUTRON_KEYPAD_LUTRON_DEVICE_ID]
441 leap_button_number = button[LUTRON_BUTTON_LEAP_BUTTON_NUMBER]
443 button_type = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get(
444 keypad_type, leap_to_keypad_button_names[keypad_device_id]
445 )[leap_button_number]
448 LUTRON_CASETA_BUTTON_EVENT,
450 ATTR_SERIAL: keypad[LUTRON_KEYPAD_SERIAL],
451 ATTR_TYPE: keypad_type,
452 ATTR_BUTTON_NUMBER: lip_button_number,
453 ATTR_LEAP_BUTTON_NUMBER: leap_button_number,
454 ATTR_DEVICE_NAME: keypad[LUTRON_KEYPAD_NAME],
455 ATTR_DEVICE_ID: keypad[LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID],
456 ATTR_AREA_NAME: keypad[LUTRON_KEYPAD_AREA_NAME],
457 ATTR_BUTTON_TYPE: button_type,
462 for button_id
in keypad_buttons:
463 bridge.add_button_subscriber(
465 lambda event_type, button_id=button_id: _async_button_event(
466 button_id, event_type
472 hass: HomeAssistant, entry: LutronCasetaConfigEntry
474 """Unload the bridge from a config entry."""
475 data = entry.runtime_data
476 await data.bridge.close()
477 return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
481 """Convert a lutron caseta identifier to a device identifier."""
482 return (DOMAIN, lutron_id)
486 hass: HomeAssistant, entry: LutronCasetaConfigEntry, device_entry: dr.DeviceEntry
488 """Remove lutron_caseta config entry from a device."""
489 data = entry.runtime_data
491 devices = bridge.get_devices()
492 buttons = bridge.buttons
493 occupancy_groups = bridge.occupancy_groups
494 bridge_device = devices[BRIDGE_DEVICE_ID]
496 all_identifiers: set[tuple[str, str]] = {
502 f
"occupancygroup_{bridge_unique_id}_{device['occupancy_group_id']}"
504 for device
in occupancy_groups.values()
509 for device
in chain(devices.values(), buttons.values())
514 for identifier
in device_entry.identifiers
515 if identifier
in all_identifiers
str serial_to_unique_id(int serial)
str area_name_from_id(dict[str, dict] areas, str|None area_id)
str _handle_none_keypad_serial(dict keypad_device, int bridge_serial)
bool async_setup_entry(HomeAssistant hass, LutronCasetaConfigEntry entry)
str _get_button_name_from_triggers(LutronKeypad keypad, int button_number)
bool async_unload_entry(HomeAssistant hass, LutronCasetaConfigEntry entry)
bool async_remove_config_entry_device(HomeAssistant hass, LutronCasetaConfigEntry entry, dr.DeviceEntry device_entry)
None _async_migrate_unique_ids(HomeAssistant hass, LutronCasetaConfigEntry entry)
tuple[str, str] _id_to_identifier(str lutron_id)
LutronKeypadData _async_setup_keypads(HomeAssistant hass, str config_entry_id, Smartbridge bridge, dict[str, str|int] bridge_device)
LutronKeypad _async_build_lutron_keypad(Smartbridge bridge, dict[str, Any] bridge_device, dict[str, Any] bridge_keypad, int keypad_device_id)
def _async_subscribe_keypad_events(HomeAssistant hass, Smartbridge bridge, dict[int, LutronKeypad] keypads, dict[int, LutronButton] keypad_buttons, dict[int, dict[int, str]] leap_to_keypad_button_names)
str _get_button_name(LutronKeypad keypad, dict[str, Any] bridge_button)
int|None async_get_lip_button(str device_type, int leap_button)
None _async_register_bridge_device(HomeAssistant hass, str config_entry_id, dict bridge_device, Smartbridge bridge)
dict[int, vol.Schema] _async_build_trigger_schemas(dict[int, dict[str, int]] keypad_button_names_to_leap)
bool async_setup(HomeAssistant hass, ConfigType base_config)