diff --git a/.coveragerc b/.coveragerc index f6368de7d89..e4fe305a3bf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -741,6 +741,7 @@ omit = homeassistant/components/lutron/binary_sensor.py homeassistant/components/lutron/cover.py homeassistant/components/lutron/entity.py + homeassistant/components/lutron/event.py homeassistant/components/lutron/fan.py homeassistant/components/lutron/light.py homeassistant/components/lutron/switch.py diff --git a/homeassistant/components/lutron/__init__.py b/homeassistant/components/lutron/__init__.py index 517eb4c8350..828182547c2 100644 --- a/homeassistant/components/lutron/__init__.py +++ b/homeassistant/components/lutron/__init__.py @@ -3,31 +3,25 @@ from dataclasses import dataclass import logging -from pylutron import Button, Keypad, Led, Lutron, LutronEvent, OccupancyGroup, Output +from pylutron import Button, Keypad, Led, Lutron, OccupancyGroup, Output import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ID, - CONF_HOST, - CONF_PASSWORD, - CONF_USERNAME, - Platform, -) +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import device_registry as dr, entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType -from homeassistant.util import slugify from .const import DOMAIN PLATFORMS = [ Platform.BINARY_SENSOR, Platform.COVER, + Platform.EVENT, Platform.FAN, Platform.LIGHT, Platform.SCENE, @@ -105,69 +99,13 @@ async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool: return True -class LutronButton: - """Representation of a button on a Lutron keypad. - - This is responsible for firing events as keypad buttons are pressed - (and possibly released, depending on the button type). It is not - represented as an entity; it simply fires events. - """ - - def __init__( - self, hass: HomeAssistant, area_name: str, keypad: Keypad, button: Button - ) -> None: - """Register callback for activity on the button.""" - name = f"{keypad.name}: {button.name}" - if button.name == "Unknown Button": - name += f" {button.number}" - self._hass = hass - self._has_release_event = ( - button.button_type is not None and "RaiseLower" in button.button_type - ) - self._id = slugify(name) - self._keypad = keypad - self._area_name = area_name - self._button_name = button.name - self._button = button - self._event = "lutron_event" - self._full_id = slugify(f"{area_name} {name}") - self._uuid = button.uuid - - button.subscribe(self.button_callback, None) - - def button_callback( - self, _button: Button, _context: None, event: LutronEvent, _params: dict - ) -> None: - """Fire an event about a button being pressed or released.""" - # Events per button type: - # RaiseLower -> pressed/released - # SingleAction -> single - action = None - if self._has_release_event: - if event == Button.Event.PRESSED: - action = "pressed" - else: - action = "released" - elif event == Button.Event.PRESSED: - action = "single" - - if action: - data = { - ATTR_ID: self._id, - ATTR_ACTION: action, - ATTR_FULL_ID: self._full_id, - ATTR_UUID: self._uuid, - } - self._hass.bus.fire(self._event, data) - - @dataclass(slots=True, kw_only=True) class LutronData: """Storage class for platform global data.""" client: Lutron binary_sensors: list[tuple[str, OccupancyGroup]] - buttons: list[LutronButton] + buttons: list[tuple[str, Keypad, Button]] covers: list[tuple[str, Output]] fans: list[tuple[str, Output]] lights: list[tuple[str, Output]] @@ -273,8 +211,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b led.legacy_uuid, entry_data.client.guid, ) - - entry_data.buttons.append(LutronButton(hass, area.name, keypad, button)) + if button.button_type: + entry_data.buttons.append((area.name, keypad, button)) if area.occupancy_group is not None: entry_data.binary_sensors.append((area.name, area.occupancy_group)) platform = Platform.BINARY_SENSOR diff --git a/homeassistant/components/lutron/event.py b/homeassistant/components/lutron/event.py new file mode 100644 index 00000000000..710f942a006 --- /dev/null +++ b/homeassistant/components/lutron/event.py @@ -0,0 +1,109 @@ +"""Support for Lutron events.""" + +from enum import StrEnum + +from pylutron import Button, Keypad, Lutron, LutronEvent + +from homeassistant.components.event import EventEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ID +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import slugify + +from . import ATTR_ACTION, ATTR_FULL_ID, ATTR_UUID, DOMAIN, LutronData +from .entity import LutronKeypad + + +class LutronEventType(StrEnum): + """Lutron event types.""" + + SINGLE_PRESS = "single_press" + PRESS = "press" + RELEASE = "release" + + +LEGACY_EVENT_TYPES: dict[LutronEventType, str] = { + LutronEventType.SINGLE_PRESS: "single", + LutronEventType.PRESS: "pressed", + LutronEventType.RELEASE: "released", +} + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Lutron event platform.""" + entry_data: LutronData = hass.data[DOMAIN][config_entry.entry_id] + + async_add_entities( + LutronEventEntity(area_name, keypad, button, entry_data.client) + for area_name, keypad, button in entry_data.buttons + ) + + +class LutronEventEntity(LutronKeypad, EventEntity): + """Representation of a Lutron keypad button.""" + + _attr_translation_key = "button" + + def __init__( + self, + area_name: str, + keypad: Keypad, + button: Button, + controller: Lutron, + ) -> None: + """Initialize the button.""" + super().__init__(area_name, button, controller, keypad) + if (name := button.name) == "Unknown Button": + name += f" {button.number}" + self._attr_name = name + self._has_release_event = ( + button.button_type is not None and "RaiseLower" in button.button_type + ) + if self._has_release_event: + self._attr_event_types = [LutronEventType.PRESS, LutronEventType.RELEASE] + else: + self._attr_event_types = [LutronEventType.SINGLE_PRESS] + + self._full_id = slugify(f"{area_name} {name}") + self._id = slugify(name) + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() + self._lutron_device.subscribe(self.handle_event, None) + + async def async_will_remove_from_hass(self) -> None: + """Unregister callbacks.""" + await super().async_will_remove_from_hass() + # Temporary solution until https://github.com/thecynic/pylutron/pull/93 gets merged + self._lutron_device._subscribers.remove((self.handle_event, None)) # pylint: disable=protected-access + + @callback + def handle_event( + self, button: Button, _context: None, event: LutronEvent, _params: dict + ) -> None: + """Handle received event.""" + action: LutronEventType | None = None + if self._has_release_event: + if event == Button.Event.PRESSED: + action = LutronEventType.PRESS + else: + action = LutronEventType.RELEASE + elif event == Button.Event.PRESSED: + action = LutronEventType.SINGLE_PRESS + + if action: + data = { + ATTR_ID: self._id, + ATTR_ACTION: LEGACY_EVENT_TYPES[action], + ATTR_FULL_ID: self._full_id, + ATTR_UUID: button.uuid, + } + self.hass.bus.fire("lutron_event", data) + self._trigger_event(action) + self.async_write_ha_state() diff --git a/homeassistant/components/lutron/strings.json b/homeassistant/components/lutron/strings.json index efa0a35d81a..0212c8845d5 100644 --- a/homeassistant/components/lutron/strings.json +++ b/homeassistant/components/lutron/strings.json @@ -22,6 +22,21 @@ "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" } }, + "entity": { + "event": { + "button": { + "state_attributes": { + "event_type": { + "state": { + "single_press": "Single press", + "press": "Press", + "release": "Release" + } + } + } + } + } + }, "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The Lutron YAML configuration import cannot connect to server",