From 0492639d51f6f841a749d2916f541bcf0b036a84 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 21:03:16 -0500 Subject: [PATCH 1/9] Add support for event entities to lutron_caseta --- .../components/lutron_caseta/__init__.py | 104 +++++++++++++----- .../components/lutron_caseta/button.py | 64 ++--------- .../components/lutron_caseta/const.py | 18 +-- .../components/lutron_caseta/event.py | 72 ++++++++++++ .../components/lutron_caseta/models.py | 30 ++++- .../lutron_caseta/test_device_trigger.py | 8 +- 6 files changed, 197 insertions(+), 99 deletions(-) create mode 100644 homeassistant/components/lutron_caseta/event.py diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 178acea83f0..d26d0f91f85 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio import contextlib +from functools import partial from itertools import chain import logging import ssl @@ -14,26 +15,19 @@ from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import ATTR_DEVICE_ID, ATTR_SUGGESTED_AREA, CONF_HOST, Platform +from homeassistant.const import ATTR_SUGGESTED_AREA, CONF_HOST, Platform from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType from .const import ( ACTION_PRESS, ACTION_RELEASE, - ATTR_ACTION, - ATTR_AREA_NAME, - ATTR_BUTTON_NUMBER, - ATTR_BUTTON_TYPE, - ATTR_DEVICE_NAME, - ATTR_LEAP_BUTTON_NUMBER, - ATTR_SERIAL, - ATTR_TYPE, BRIDGE_DEVICE_ID, BRIDGE_TIMEOUT, CONF_CA_CERTS, @@ -63,6 +57,8 @@ from .models import ( LUTRON_KEYPAD_SERIAL, LUTRON_KEYPAD_TYPE, LutronButton, + LutronCasetaButtonDevice, + LutronCasetaButtonEventData, LutronCasetaConfigEntry, LutronCasetaData, LutronKeypad, @@ -95,6 +91,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.BUTTON, Platform.COVER, + Platform.EVENT, Platform.FAN, Platform.LIGHT, Platform.SCENE, @@ -200,14 +197,65 @@ async def async_setup_entry( # Store this bridge (keyed by entry_id) so it can be retrieved by the # platforms we're setting up. + button_devices = _async_build_button_devices(bridge, keypad_data) - entry.runtime_data = LutronCasetaData(bridge, bridge_device, keypad_data) + entry.runtime_data = LutronCasetaData( + bridge, bridge_device, keypad_data, button_devices + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True +@callback +def _async_build_button_devices( + bridge: Smartbridge, keypad_data: LutronKeypadData +) -> list[LutronCasetaButtonDevice]: + button_devices = bridge.get_buttons() + all_devices = bridge.get_devices() + keypads = keypad_data.keypads + buttons: list[LutronCasetaButtonDevice] = [] + + for button_id, device in button_devices.items(): + parent_keypad = keypads[device["parent_device"]] + parent_device_info = parent_keypad["device_info"] + parent_name = parent_device_info["name"] + + has_device_name = True + if not (device_name := device.get("device_name")): + # device name (button name) is missing, probably a caseta pico + # try to get the name using the button number from the triggers + # disable the button by default + has_device_name = False + keypad_device = all_devices[device["parent_device"]] + button_numbers = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get( + keypad_device["type"], + {}, + ) + device_name = ( + button_numbers.get( + int(device["button_number"]), + f"button {device['button_number']}", + ) + .replace("_", " ") + .title() + ) + + # Append the child device name to the end of the parent keypad + # name to create the entity name + full_name = f"{parent_name} {device_name}" + # Set the device_info to the same as the Parent Keypad + # The entities will be nested inside the keypad device + buttons.append( + LutronCasetaButtonDevice( + button_id, device, full_name, has_device_name, parent_device_info + ), + ) + + return buttons + + @callback def _async_register_bridge_device( hass: HomeAssistant, config_entry_id: str, bridge_device: dict, bridge: Smartbridge @@ -301,6 +349,7 @@ def _async_setup_keypads( _async_subscribe_keypad_events( hass=hass, + config_entry_id=config_entry_id, bridge=bridge, keypads=keypads, keypad_buttons=keypad_buttons, @@ -440,15 +489,16 @@ def async_get_lip_button(device_type: str, leap_button: int) -> int | None: @callback def _async_subscribe_keypad_events( hass: HomeAssistant, + config_entry_id: str, bridge: Smartbridge, keypads: dict[int, LutronKeypad], keypad_buttons: dict[int, LutronButton], leap_to_keypad_button_names: dict[int, dict[int, str]], -): +) -> None: """Subscribe to lutron events.""" @callback - def _async_button_event(button_id, event_type): + def _async_button_event(button_id: int, event_type: str) -> None: if not (button := keypad_buttons.get(button_id)) or not ( keypad := keypads.get(button["parent_keypad"]) ): @@ -467,27 +517,23 @@ def _async_subscribe_keypad_events( keypad_type, leap_to_keypad_button_names[keypad_device_id] )[leap_button_number] - hass.bus.async_fire( - LUTRON_CASETA_BUTTON_EVENT, - { - ATTR_SERIAL: keypad[LUTRON_KEYPAD_SERIAL], - ATTR_TYPE: keypad_type, - ATTR_BUTTON_NUMBER: lip_button_number, - ATTR_LEAP_BUTTON_NUMBER: leap_button_number, - ATTR_DEVICE_NAME: keypad[LUTRON_KEYPAD_NAME], - ATTR_DEVICE_ID: keypad[LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID], - ATTR_AREA_NAME: keypad[LUTRON_KEYPAD_AREA_NAME], - ATTR_BUTTON_TYPE: button_type, - ATTR_ACTION: action, - }, + data = LutronCasetaButtonEventData( + serial=keypad[LUTRON_KEYPAD_SERIAL], + type=keypad_type, + button_number=lip_button_number, + leap_button_number=leap_button_number, + device_name=keypad[LUTRON_KEYPAD_NAME], + device_id=keypad[LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID], + area_name=keypad[LUTRON_KEYPAD_AREA_NAME], + button_type=button_type, + action=action, ) + async_dispatcher_send(f"{DOMAIN}_{config_entry_id}_button_{button_id}", data) + hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, data) for button_id in keypad_buttons: bridge.add_button_subscriber( - str(button_id), - lambda event_type, button_id=button_id: _async_button_event( - button_id, event_type - ), + str(button_id), partial(_async_button_event, button_id) ) diff --git a/homeassistant/components/lutron_caseta/button.py b/homeassistant/components/lutron_caseta/button.py index d2651673c4c..297a321aca8 100644 --- a/homeassistant/components/lutron_caseta/button.py +++ b/homeassistant/components/lutron_caseta/button.py @@ -2,16 +2,12 @@ from __future__ import annotations -from typing import Any - from homeassistant.components.button import ButtonEntity from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LutronCasetaDevice -from .device_trigger import LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP -from .models import LutronCasetaConfigEntry, LutronCasetaData +from .models import LutronCasetaButtonDevice, LutronCasetaConfigEntry, LutronCasetaData async def async_setup_entry( @@ -21,48 +17,9 @@ async def async_setup_entry( ) -> None: """Set up Lutron pico and keypad buttons.""" data = config_entry.runtime_data - bridge = data.bridge - button_devices = bridge.get_buttons() - all_devices = data.bridge.get_devices() - keypads = data.keypad_data.keypads - entities: list[LutronCasetaButton] = [] - - for device in button_devices.values(): - parent_keypad = keypads[device["parent_device"]] - parent_device_info = parent_keypad["device_info"] - - enabled_default = True - if not (device_name := device.get("device_name")): - # device name (button name) is missing, probably a caseta pico - # try to get the name using the button number from the triggers - # disable the button by default - enabled_default = False - keypad_device = all_devices[device["parent_device"]] - button_numbers = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get( - keypad_device["type"], - {}, - ) - device_name = ( - button_numbers.get( - int(device["button_number"]), - f"button {device['button_number']}", - ) - .replace("_", " ") - .title() - ) - - # Append the child device name to the end of the parent keypad - # name to create the entity name - full_name = f'{parent_device_info.get("name")} {device_name}' - # Set the device_info to the same as the Parent Keypad - # The entities will be nested inside the keypad device - entities.append( - LutronCasetaButton( - device, data, full_name, enabled_default, parent_device_info - ), - ) - - async_add_entities(entities) + async_add_entities( + LutronCasetaButton(data, button_device) for button_device in data.button_devices + ) class LutronCasetaButton(LutronCasetaDevice, ButtonEntity): @@ -70,17 +27,14 @@ class LutronCasetaButton(LutronCasetaDevice, ButtonEntity): def __init__( self, - device: dict[str, Any], data: LutronCasetaData, - full_name: str, - enabled_default: bool, - device_info: DeviceInfo, + button_device: LutronCasetaButtonDevice, ) -> None: """Init a button entity.""" - super().__init__(device, data) - self._attr_entity_registry_enabled_default = enabled_default - self._attr_name = full_name - self._attr_device_info = device_info + super().__init__(button_device.device, data) + self._attr_entity_registry_enabled_default = button_device.has_device_name + self._attr_name = button_device.full_name + self._attr_device_info = button_device.parent_device_info async def async_press(self) -> None: """Send a button press event.""" diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 7493878bece..ce9c7ab2f08 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -1,5 +1,7 @@ """Lutron Caseta constants.""" +from typing import Final + DOMAIN = "lutron_caseta" CONF_KEYFILE = "keyfile" @@ -19,14 +21,14 @@ DEVICE_TYPE_SPECTRUM_TUNE = "SpectrumTune" MANUFACTURER = "Lutron Electronics Co., Inc" -ATTR_SERIAL = "serial" -ATTR_TYPE = "type" -ATTR_BUTTON_TYPE = "button_type" -ATTR_LEAP_BUTTON_NUMBER = "leap_button_number" -ATTR_BUTTON_NUMBER = "button_number" # LIP button number -ATTR_DEVICE_NAME = "device_name" -ATTR_AREA_NAME = "area_name" -ATTR_ACTION = "action" +ATTR_SERIAL: Final = "serial" +ATTR_TYPE: Final = "type" +ATTR_BUTTON_TYPE: Final = "button_type" +ATTR_LEAP_BUTTON_NUMBER: Final = "leap_button_number" +ATTR_BUTTON_NUMBER: Final = "button_number" # LIP button number +ATTR_DEVICE_NAME: Final = "device_name" +ATTR_AREA_NAME: Final = "area_name" +ATTR_ACTION: Final = "action" ACTION_PRESS = "press" ACTION_RELEASE = "release" diff --git a/homeassistant/components/lutron_caseta/event.py b/homeassistant/components/lutron_caseta/event.py new file mode 100644 index 00000000000..eadc9e1f6ac --- /dev/null +++ b/homeassistant/components/lutron_caseta/event.py @@ -0,0 +1,72 @@ +"""Support for pico and keypad button events.""" + +from __future__ import annotations + +from homeassistant.components.event import EventEntity +from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import LutronCasetaDevice +from .const import DOMAIN +from .models import ( + LutronCasetaButtonDevice, + LutronCasetaButtonEventData, + LutronCasetaConfigEntry, + LutronCasetaData, +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: LutronCasetaConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Lutron pico and keypad buttons.""" + data = config_entry.runtime_data + async_add_entities( + LutronCasetaButtonEvent(data, config_entry.entry_id, button_device) + for button_device in data.button_devices + ) + + +class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): + """Representation of a Lutron pico and keypad button event.""" + + def __init__( + self, + data: LutronCasetaData, + button_device: LutronCasetaButtonDevice, + entry_id: str, + ) -> None: + """Init a button event entity.""" + super().__init__(button_device.device, data) + self._attr_name = button_device.full_name + self._attr_device_info = button_device.parent_device_info + self._button_id = button_device.button_id + self._entry_id = entry_id + + async def async_press(self) -> None: + """Send a button press event.""" + await self._smartbridge.tap_button(self.device_id) + + @property + def serial(self): + """Buttons shouldn't have serial numbers, Return None.""" + return None + + def _handle_button_event(self, data: LutronCasetaButtonEventData) -> None: + """Handle a button event.""" + self._trigger_event(data["action"]) + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Subscribe to button events.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self._entry_id}_button_{self._button_id}", + self._handle_button_event, + ) + ) + await super().async_added_to_hass() diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 402fa8885e8..d35b5d5d2ff 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -14,16 +14,17 @@ from homeassistant.helpers.device_registry import DeviceInfo type LutronCasetaConfigEntry = ConfigEntry[LutronCasetaData] -@dataclass +@dataclass(slots=True) class LutronCasetaData: """Data for the lutron_caseta integration.""" bridge: Smartbridge bridge_device: dict[str, Any] keypad_data: LutronKeypadData + button_devices: list[LutronCasetaButtonDevice] -@dataclass +@dataclass(slots=True) class LutronKeypadData: """Data for the lutron_caseta integration keypads.""" @@ -34,6 +35,31 @@ class LutronKeypadData: trigger_schemas: dict[int, vol.Schema] +@dataclass(slots=True) +class LutronCasetaButtonDevice: + """A lutron_caseta button device.""" + + button_id: int + device: dict + full_name: str + has_device_name: bool + parent_device_info: DeviceInfo + + +class LutronCasetaButtonEventData(TypedDict): + """A lutron_caseta button event data.""" + + serial: str + type: str + button_number: int + leap_button_number: int + device_name: str + device_id: str + area_name: str + button_type: str + action: str + + class LutronKeypad(TypedDict): """A lutron_caseta keypad device.""" diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 405c504dee1..5b9a04a6557 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -7,16 +7,14 @@ from pytest_unordered import unordered from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType -from homeassistant.components.lutron_caseta import ( +from homeassistant.components.lutron_caseta.const import ( ATTR_ACTION, ATTR_AREA_NAME, + ATTR_BUTTON_TYPE, ATTR_DEVICE_NAME, + ATTR_LEAP_BUTTON_NUMBER, ATTR_SERIAL, ATTR_TYPE, -) -from homeassistant.components.lutron_caseta.const import ( - ATTR_BUTTON_TYPE, - ATTR_LEAP_BUTTON_NUMBER, CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE, From 581efad5a730b353bb64a2cebf338ecef5ea31ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 21:18:01 -0500 Subject: [PATCH 2/9] lint --- homeassistant/components/lutron_caseta/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index d35b5d5d2ff..7c3e9831e4b 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -51,7 +51,7 @@ class LutronCasetaButtonEventData(TypedDict): serial: str type: str - button_number: int + button_number: int | None leap_button_number: int device_name: str device_id: str From 93fe46509f22340158974f9704a37b601a5a5b2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 21:19:07 -0500 Subject: [PATCH 3/9] lint --- homeassistant/components/lutron_caseta/__init__.py | 4 +++- homeassistant/components/lutron_caseta/event.py | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index d26d0f91f85..03f5245199c 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -528,7 +528,9 @@ def _async_subscribe_keypad_events( button_type=button_type, action=action, ) - async_dispatcher_send(f"{DOMAIN}_{config_entry_id}_button_{button_id}", data) + async_dispatcher_send( + hass, f"{DOMAIN}_{config_entry_id}_button_{button_id}", data + ) hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, data) for button_id in keypad_buttons: diff --git a/homeassistant/components/lutron_caseta/event.py b/homeassistant/components/lutron_caseta/event.py index eadc9e1f6ac..de276df2d16 100644 --- a/homeassistant/components/lutron_caseta/event.py +++ b/homeassistant/components/lutron_caseta/event.py @@ -2,7 +2,7 @@ from __future__ import annotations -from homeassistant.components.event import EventEntity +from homeassistant.components.event import EventDeviceClass, EventEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -25,7 +25,7 @@ async def async_setup_entry( """Set up Lutron pico and keypad buttons.""" data = config_entry.runtime_data async_add_entities( - LutronCasetaButtonEvent(data, config_entry.entry_id, button_device) + LutronCasetaButtonEvent(data, button_device, config_entry.entry_id) for button_device in data.button_devices ) @@ -33,6 +33,8 @@ async def async_setup_entry( class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): """Representation of a Lutron pico and keypad button event.""" + _attr_device_class = EventDeviceClass.BUTTON + def __init__( self, data: LutronCasetaData, From 0c2a0118e208033eee06887145dc998c87ffdea0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 21:23:10 -0500 Subject: [PATCH 4/9] event types --- homeassistant/components/lutron_caseta/event.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/lutron_caseta/event.py b/homeassistant/components/lutron_caseta/event.py index de276df2d16..6193283f77b 100644 --- a/homeassistant/components/lutron_caseta/event.py +++ b/homeassistant/components/lutron_caseta/event.py @@ -2,6 +2,8 @@ from __future__ import annotations +from pylutron_caseta import BUTTON_STATUS_PRESSED, BUTTON_STATUS_RELEASED + from homeassistant.components.event import EventDeviceClass, EventEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -34,6 +36,7 @@ class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): """Representation of a Lutron pico and keypad button event.""" _attr_device_class = EventDeviceClass.BUTTON + _attr_event_types = [BUTTON_STATUS_PRESSED, BUTTON_STATUS_RELEASED] def __init__( self, From 2208262ca50a78bb98e7b7c60b73eb2571483612 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 21:27:17 -0500 Subject: [PATCH 5/9] debug --- homeassistant/components/lutron_caseta/__init__.py | 6 +++--- homeassistant/components/lutron_caseta/event.py | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 03f5245199c..90b9b528a98 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -528,9 +528,9 @@ def _async_subscribe_keypad_events( button_type=button_type, action=action, ) - async_dispatcher_send( - hass, f"{DOMAIN}_{config_entry_id}_button_{button_id}", data - ) + signal = f"{DOMAIN}_{config_entry_id}_button_{button_id}" + _LOGGER.warning("Sending signal: %s - %s", signal, data) + async_dispatcher_send(hass, signal, data) hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, data) for button_id in keypad_buttons: diff --git a/homeassistant/components/lutron_caseta/event.py b/homeassistant/components/lutron_caseta/event.py index 6193283f77b..58be305ce0b 100644 --- a/homeassistant/components/lutron_caseta/event.py +++ b/homeassistant/components/lutron_caseta/event.py @@ -2,6 +2,8 @@ from __future__ import annotations +import logging + from pylutron_caseta import BUTTON_STATUS_PRESSED, BUTTON_STATUS_RELEASED from homeassistant.components.event import EventDeviceClass, EventEntity @@ -18,6 +20,8 @@ from .models import ( LutronCasetaData, ) +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -67,6 +71,10 @@ class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): async def async_added_to_hass(self) -> None: """Subscribe to button events.""" + _LOGGER.warning( + "Subscribing to button %s", + f"{DOMAIN}_{self._entry_id}_button_{self._button_id}", + ) self.async_on_remove( async_dispatcher_connect( self.hass, From 15d8b84074b0f6a99d3d41b48ca95c5ce214d39d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 21:31:36 -0500 Subject: [PATCH 6/9] adjust --- homeassistant/components/lutron_caseta/__init__.py | 1 - homeassistant/components/lutron_caseta/event.py | 10 ++-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 90b9b528a98..9ab62dce7c6 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -529,7 +529,6 @@ def _async_subscribe_keypad_events( action=action, ) signal = f"{DOMAIN}_{config_entry_id}_button_{button_id}" - _LOGGER.warning("Sending signal: %s - %s", signal, data) async_dispatcher_send(hass, signal, data) hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, data) diff --git a/homeassistant/components/lutron_caseta/event.py b/homeassistant/components/lutron_caseta/event.py index 58be305ce0b..10b42298c55 100644 --- a/homeassistant/components/lutron_caseta/event.py +++ b/homeassistant/components/lutron_caseta/event.py @@ -4,15 +4,13 @@ from __future__ import annotations import logging -from pylutron_caseta import BUTTON_STATUS_PRESSED, BUTTON_STATUS_RELEASED - from homeassistant.components.event import EventDeviceClass, EventEntity from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import LutronCasetaDevice -from .const import DOMAIN +from .const import ACTION_PRESS, ACTION_RELEASE, DOMAIN from .models import ( LutronCasetaButtonDevice, LutronCasetaButtonEventData, @@ -40,7 +38,7 @@ class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): """Representation of a Lutron pico and keypad button event.""" _attr_device_class = EventDeviceClass.BUTTON - _attr_event_types = [BUTTON_STATUS_PRESSED, BUTTON_STATUS_RELEASED] + _attr_event_types = [ACTION_PRESS, ACTION_RELEASE] def __init__( self, @@ -71,10 +69,6 @@ class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): async def async_added_to_hass(self) -> None: """Subscribe to button events.""" - _LOGGER.warning( - "Subscribing to button %s", - f"{DOMAIN}_{self._entry_id}_button_{self._button_id}", - ) self.async_on_remove( async_dispatcher_connect( self.hass, From 4d01e0a773dc18c717bb6f6f65e8091c88a8bd5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 21:43:08 -0500 Subject: [PATCH 7/9] cleanup --- .../components/lutron_caseta/__init__.py | 31 ++++++++++++------- .../components/lutron_caseta/button.py | 6 ++-- .../components/lutron_caseta/event.py | 4 ++- .../components/lutron_caseta/models.py | 4 ++- 4 files changed, 29 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 9ab62dce7c6..0ffbe16ee19 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -222,34 +222,41 @@ def _async_build_button_devices( parent_device_info = parent_keypad["device_info"] parent_name = parent_device_info["name"] - has_device_name = True - if not (device_name := device.get("device_name")): + button_key: str | None = None + button_name: str + device_name = cast(str | None, device.get("device_name")) + user_defined_name = bool(device_name) + if device_name: + button_name = device_name + else: # device name (button name) is missing, probably a caseta pico # try to get the name using the button number from the triggers # disable the button by default - has_device_name = False keypad_device = all_devices[device["parent_device"]] button_numbers = LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP.get( keypad_device["type"], {}, ) - device_name = ( - button_numbers.get( - int(device["button_number"]), - f"button {device['button_number']}", - ) - .replace("_", " ") - .title() + button_key = button_numbers.get(int(device["button_number"])) + button_name = ( + button_key + or f"button {device['button_number']}".replace("_", " ").title() ) # Append the child device name to the end of the parent keypad # name to create the entity name - full_name = f"{parent_name} {device_name}" + full_name = f"{parent_name} {button_name}" # Set the device_info to the same as the Parent Keypad # The entities will be nested inside the keypad device buttons.append( LutronCasetaButtonDevice( - button_id, device, full_name, has_device_name, parent_device_info + button_id, + device, + button_key, + button_name, + full_name, + user_defined_name, + parent_device_info, ), ) diff --git a/homeassistant/components/lutron_caseta/button.py b/homeassistant/components/lutron_caseta/button.py index 297a321aca8..335ae6eb0ec 100644 --- a/homeassistant/components/lutron_caseta/button.py +++ b/homeassistant/components/lutron_caseta/button.py @@ -25,6 +25,8 @@ async def async_setup_entry( class LutronCasetaButton(LutronCasetaDevice, ButtonEntity): """Representation of a Lutron pico and keypad button.""" + _attr_has_entity_name = True + def __init__( self, data: LutronCasetaData, @@ -32,8 +34,8 @@ class LutronCasetaButton(LutronCasetaDevice, ButtonEntity): ) -> None: """Init a button entity.""" super().__init__(button_device.device, data) - self._attr_entity_registry_enabled_default = button_device.has_device_name - self._attr_name = button_device.full_name + self._attr_entity_registry_enabled_default = button_device.user_defined_name + self._attr_name = button_device.button_name self._attr_device_info = button_device.parent_device_info async def async_press(self) -> None: diff --git a/homeassistant/components/lutron_caseta/event.py b/homeassistant/components/lutron_caseta/event.py index 10b42298c55..0332b1e888f 100644 --- a/homeassistant/components/lutron_caseta/event.py +++ b/homeassistant/components/lutron_caseta/event.py @@ -39,6 +39,7 @@ class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): _attr_device_class = EventDeviceClass.BUTTON _attr_event_types = [ACTION_PRESS, ACTION_RELEASE] + _attr_has_entity_name = True def __init__( self, @@ -48,7 +49,8 @@ class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): ) -> None: """Init a button event entity.""" super().__init__(button_device.device, data) - self._attr_name = button_device.full_name + self._attr_name = button_device.button_name + self._attr_translation_key = button_device.button_key self._attr_device_info = button_device.parent_device_info self._button_id = button_device.button_id self._entry_id = entry_id diff --git a/homeassistant/components/lutron_caseta/models.py b/homeassistant/components/lutron_caseta/models.py index 7c3e9831e4b..3f9f336d097 100644 --- a/homeassistant/components/lutron_caseta/models.py +++ b/homeassistant/components/lutron_caseta/models.py @@ -41,8 +41,10 @@ class LutronCasetaButtonDevice: button_id: int device: dict + button_key: str | None + button_name: str full_name: str - has_device_name: bool + user_defined_name: bool parent_device_info: DeviceInfo From 148bb05dea0ba762e26fbacd79c91a0e439ca85f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 29 Jul 2024 21:47:55 -0500 Subject: [PATCH 8/9] loop --- homeassistant/components/lutron_caseta/event.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/lutron_caseta/event.py b/homeassistant/components/lutron_caseta/event.py index 0332b1e888f..3c0a8f292d5 100644 --- a/homeassistant/components/lutron_caseta/event.py +++ b/homeassistant/components/lutron_caseta/event.py @@ -2,10 +2,8 @@ from __future__ import annotations -import logging - from homeassistant.components.event import EventDeviceClass, EventEntity -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -18,8 +16,6 @@ from .models import ( LutronCasetaData, ) -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -55,16 +51,13 @@ class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): self._button_id = button_device.button_id self._entry_id = entry_id - async def async_press(self) -> None: - """Send a button press event.""" - await self._smartbridge.tap_button(self.device_id) - @property def serial(self): """Buttons shouldn't have serial numbers, Return None.""" return None - def _handle_button_event(self, data: LutronCasetaButtonEventData) -> None: + @callback + def _async_handle_button_event(self, data: LutronCasetaButtonEventData) -> None: """Handle a button event.""" self._trigger_event(data["action"]) self.async_write_ha_state() @@ -75,7 +68,7 @@ class LutronCasetaButtonEvent(LutronCasetaDevice, EventEntity): async_dispatcher_connect( self.hass, f"{DOMAIN}_{self._entry_id}_button_{self._button_id}", - self._handle_button_event, + self._async_handle_button_event, ) ) await super().async_added_to_hass() From 87a00eb80f238a69c0441b77aeb03b8885764d57 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Sep 2024 11:40:01 -0500 Subject: [PATCH 9/9] merge --- tests/components/lutron_caseta/__init__.py | 13 ++++++++++--- .../components/lutron_caseta/test_device_trigger.py | 1 + 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index b27d30ac31f..bb7131819aa 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -1,5 +1,6 @@ """Tests for the Lutron Caseta integration.""" +from collections import defaultdict from unittest.mock import patch from homeassistant.components.lutron_caseta import DOMAIN @@ -84,7 +85,9 @@ _LEAP_DEVICE_TYPES = { } -async def async_setup_integration(hass: HomeAssistant, mock_bridge) -> MockConfigEntry: +async def async_setup_integration( + hass: HomeAssistant, mock_bridge +) -> tuple[MockConfigEntry, "MockBridge"]: """Set up a mock bridge.""" mock_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA) mock_entry.add_to_hass(hass) @@ -92,10 +95,12 @@ async def async_setup_integration(hass: HomeAssistant, mock_bridge) -> MockConfi with patch( "homeassistant.components.lutron_caseta.Smartbridge.create_tls" ) as create_tls: - create_tls.return_value = mock_bridge(can_connect=True) + mocked_bridge = mock_bridge(can_connect=True) + create_tls.return_value = mocked_bridge await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - return mock_entry + + return mock_entry, mocked_bridge class MockBridge: @@ -110,6 +115,7 @@ class MockBridge: self.scenes = self.get_scenes() self.devices = self.load_devices() self.buttons = self.load_buttons() + self.button_subscribers: defaultdict[str, list] = defaultdict(list) async def connect(self): """Connect the mock bridge.""" @@ -121,6 +127,7 @@ class MockBridge: def add_button_subscriber(self, button_id: str, callback_): """Mock a listener for button presses.""" + self.button_subscribers[button_id].append(callback_) def is_connected(self): """Return whether the mock bridge is connected.""" diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py index 5b1dc7ae381..04eac003603 100644 --- a/tests/components/lutron_caseta/test_device_trigger.py +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -157,6 +157,7 @@ async def test_get_triggers(hass: HomeAssistant) -> None: triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, device_id ) + triggers = [trigger for trigger in triggers if trigger[CONF_DOMAIN] == DOMAIN] assert triggers == unordered(expected_triggers)