Add support for event entities to lutron_caseta

This commit is contained in:
J. Nick Koston 2024-07-29 21:03:16 -05:00
parent 004eccec89
commit 0492639d51
No known key found for this signature in database
6 changed files with 197 additions and 99 deletions

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import asyncio import asyncio
import contextlib import contextlib
from functools import partial
from itertools import chain from itertools import chain
import logging import logging
import ssl import ssl
@ -14,26 +15,19 @@ from pylutron_caseta.smartbridge import Smartbridge
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries 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.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers import device_registry as dr, entity_registry as er
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
ACTION_PRESS, ACTION_PRESS,
ACTION_RELEASE, 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_DEVICE_ID,
BRIDGE_TIMEOUT, BRIDGE_TIMEOUT,
CONF_CA_CERTS, CONF_CA_CERTS,
@ -63,6 +57,8 @@ from .models import (
LUTRON_KEYPAD_SERIAL, LUTRON_KEYPAD_SERIAL,
LUTRON_KEYPAD_TYPE, LUTRON_KEYPAD_TYPE,
LutronButton, LutronButton,
LutronCasetaButtonDevice,
LutronCasetaButtonEventData,
LutronCasetaConfigEntry, LutronCasetaConfigEntry,
LutronCasetaData, LutronCasetaData,
LutronKeypad, LutronKeypad,
@ -95,6 +91,7 @@ PLATFORMS = [
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.BUTTON, Platform.BUTTON,
Platform.COVER, Platform.COVER,
Platform.EVENT,
Platform.FAN, Platform.FAN,
Platform.LIGHT, Platform.LIGHT,
Platform.SCENE, 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 # Store this bridge (keyed by entry_id) so it can be retrieved by the
# platforms we're setting up. # 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) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True 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 @callback
def _async_register_bridge_device( def _async_register_bridge_device(
hass: HomeAssistant, config_entry_id: str, bridge_device: dict, bridge: Smartbridge hass: HomeAssistant, config_entry_id: str, bridge_device: dict, bridge: Smartbridge
@ -301,6 +349,7 @@ def _async_setup_keypads(
_async_subscribe_keypad_events( _async_subscribe_keypad_events(
hass=hass, hass=hass,
config_entry_id=config_entry_id,
bridge=bridge, bridge=bridge,
keypads=keypads, keypads=keypads,
keypad_buttons=keypad_buttons, keypad_buttons=keypad_buttons,
@ -440,15 +489,16 @@ def async_get_lip_button(device_type: str, leap_button: int) -> int | None:
@callback @callback
def _async_subscribe_keypad_events( def _async_subscribe_keypad_events(
hass: HomeAssistant, hass: HomeAssistant,
config_entry_id: str,
bridge: Smartbridge, bridge: Smartbridge,
keypads: dict[int, LutronKeypad], keypads: dict[int, LutronKeypad],
keypad_buttons: dict[int, LutronButton], keypad_buttons: dict[int, LutronButton],
leap_to_keypad_button_names: dict[int, dict[int, str]], leap_to_keypad_button_names: dict[int, dict[int, str]],
): ) -> None:
"""Subscribe to lutron events.""" """Subscribe to lutron events."""
@callback @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 ( if not (button := keypad_buttons.get(button_id)) or not (
keypad := keypads.get(button["parent_keypad"]) 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] keypad_type, leap_to_keypad_button_names[keypad_device_id]
)[leap_button_number] )[leap_button_number]
hass.bus.async_fire( data = LutronCasetaButtonEventData(
LUTRON_CASETA_BUTTON_EVENT, serial=keypad[LUTRON_KEYPAD_SERIAL],
{ type=keypad_type,
ATTR_SERIAL: keypad[LUTRON_KEYPAD_SERIAL], button_number=lip_button_number,
ATTR_TYPE: keypad_type, leap_button_number=leap_button_number,
ATTR_BUTTON_NUMBER: lip_button_number, device_name=keypad[LUTRON_KEYPAD_NAME],
ATTR_LEAP_BUTTON_NUMBER: leap_button_number, device_id=keypad[LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID],
ATTR_DEVICE_NAME: keypad[LUTRON_KEYPAD_NAME], area_name=keypad[LUTRON_KEYPAD_AREA_NAME],
ATTR_DEVICE_ID: keypad[LUTRON_KEYPAD_DEVICE_REGISTRY_DEVICE_ID], button_type=button_type,
ATTR_AREA_NAME: keypad[LUTRON_KEYPAD_AREA_NAME], action=action,
ATTR_BUTTON_TYPE: button_type,
ATTR_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: for button_id in keypad_buttons:
bridge.add_button_subscriber( bridge.add_button_subscriber(
str(button_id), str(button_id), partial(_async_button_event, button_id)
lambda event_type, button_id=button_id: _async_button_event(
button_id, event_type
),
) )

View File

@ -2,16 +2,12 @@
from __future__ import annotations from __future__ import annotations
from typing import Any
from homeassistant.components.button import ButtonEntity from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import LutronCasetaDevice from . import LutronCasetaDevice
from .device_trigger import LEAP_TO_DEVICE_TYPE_SUBTYPE_MAP from .models import LutronCasetaButtonDevice, LutronCasetaConfigEntry, LutronCasetaData
from .models import LutronCasetaConfigEntry, LutronCasetaData
async def async_setup_entry( async def async_setup_entry(
@ -21,48 +17,9 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up Lutron pico and keypad buttons.""" """Set up Lutron pico and keypad buttons."""
data = config_entry.runtime_data data = config_entry.runtime_data
bridge = data.bridge async_add_entities(
button_devices = bridge.get_buttons() LutronCasetaButton(data, button_device) for button_device in data.button_devices
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)
class LutronCasetaButton(LutronCasetaDevice, ButtonEntity): class LutronCasetaButton(LutronCasetaDevice, ButtonEntity):
@ -70,17 +27,14 @@ class LutronCasetaButton(LutronCasetaDevice, ButtonEntity):
def __init__( def __init__(
self, self,
device: dict[str, Any],
data: LutronCasetaData, data: LutronCasetaData,
full_name: str, button_device: LutronCasetaButtonDevice,
enabled_default: bool,
device_info: DeviceInfo,
) -> None: ) -> None:
"""Init a button entity.""" """Init a button entity."""
super().__init__(device, data) super().__init__(button_device.device, data)
self._attr_entity_registry_enabled_default = enabled_default self._attr_entity_registry_enabled_default = button_device.has_device_name
self._attr_name = full_name self._attr_name = button_device.full_name
self._attr_device_info = device_info self._attr_device_info = button_device.parent_device_info
async def async_press(self) -> None: async def async_press(self) -> None:
"""Send a button press event.""" """Send a button press event."""

View File

@ -1,5 +1,7 @@
"""Lutron Caseta constants.""" """Lutron Caseta constants."""
from typing import Final
DOMAIN = "lutron_caseta" DOMAIN = "lutron_caseta"
CONF_KEYFILE = "keyfile" CONF_KEYFILE = "keyfile"
@ -19,14 +21,14 @@ DEVICE_TYPE_SPECTRUM_TUNE = "SpectrumTune"
MANUFACTURER = "Lutron Electronics Co., Inc" MANUFACTURER = "Lutron Electronics Co., Inc"
ATTR_SERIAL = "serial" ATTR_SERIAL: Final = "serial"
ATTR_TYPE = "type" ATTR_TYPE: Final = "type"
ATTR_BUTTON_TYPE = "button_type" ATTR_BUTTON_TYPE: Final = "button_type"
ATTR_LEAP_BUTTON_NUMBER = "leap_button_number" ATTR_LEAP_BUTTON_NUMBER: Final = "leap_button_number"
ATTR_BUTTON_NUMBER = "button_number" # LIP button number ATTR_BUTTON_NUMBER: Final = "button_number" # LIP button number
ATTR_DEVICE_NAME = "device_name" ATTR_DEVICE_NAME: Final = "device_name"
ATTR_AREA_NAME = "area_name" ATTR_AREA_NAME: Final = "area_name"
ATTR_ACTION = "action" ATTR_ACTION: Final = "action"
ACTION_PRESS = "press" ACTION_PRESS = "press"
ACTION_RELEASE = "release" ACTION_RELEASE = "release"

View File

@ -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()

View File

@ -14,16 +14,17 @@ from homeassistant.helpers.device_registry import DeviceInfo
type LutronCasetaConfigEntry = ConfigEntry[LutronCasetaData] type LutronCasetaConfigEntry = ConfigEntry[LutronCasetaData]
@dataclass @dataclass(slots=True)
class LutronCasetaData: class LutronCasetaData:
"""Data for the lutron_caseta integration.""" """Data for the lutron_caseta integration."""
bridge: Smartbridge bridge: Smartbridge
bridge_device: dict[str, Any] bridge_device: dict[str, Any]
keypad_data: LutronKeypadData keypad_data: LutronKeypadData
button_devices: list[LutronCasetaButtonDevice]
@dataclass @dataclass(slots=True)
class LutronKeypadData: class LutronKeypadData:
"""Data for the lutron_caseta integration keypads.""" """Data for the lutron_caseta integration keypads."""
@ -34,6 +35,31 @@ class LutronKeypadData:
trigger_schemas: dict[int, vol.Schema] 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): class LutronKeypad(TypedDict):
"""A lutron_caseta keypad device.""" """A lutron_caseta keypad device."""

View File

@ -7,16 +7,14 @@ from pytest_unordered import unordered
from homeassistant.components import automation from homeassistant.components import automation
from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation import DeviceAutomationType
from homeassistant.components.lutron_caseta import ( from homeassistant.components.lutron_caseta.const import (
ATTR_ACTION, ATTR_ACTION,
ATTR_AREA_NAME, ATTR_AREA_NAME,
ATTR_BUTTON_TYPE,
ATTR_DEVICE_NAME, ATTR_DEVICE_NAME,
ATTR_LEAP_BUTTON_NUMBER,
ATTR_SERIAL, ATTR_SERIAL,
ATTR_TYPE, ATTR_TYPE,
)
from homeassistant.components.lutron_caseta.const import (
ATTR_BUTTON_TYPE,
ATTR_LEAP_BUTTON_NUMBER,
CONF_CA_CERTS, CONF_CA_CERTS,
CONF_CERTFILE, CONF_CERTFILE,
CONF_KEYFILE, CONF_KEYFILE,