core/homeassistant/components/lutron/__init__.py

360 lines
12 KiB
Python

"""Component for interacting with a Lutron RadioRA 2 system."""
from dataclasses import dataclass
import logging
from pylutron import Button, Keypad, Led, Lutron, LutronEvent, 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.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.FAN,
Platform.LIGHT,
Platform.SCENE,
Platform.SWITCH,
]
_LOGGER = logging.getLogger(__name__)
# Attribute on events that indicates what action was taken with the button.
ATTR_ACTION = "action"
ATTR_FULL_ID = "full_id"
ATTR_UUID = "uuid"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def _async_import(hass: HomeAssistant, base_config: ConfigType) -> None:
"""Import a config entry from configuration.yaml."""
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=base_config[DOMAIN],
)
if (
result["type"] == FlowResultType.CREATE_ENTRY
or result["reason"] == "single_instance_allowed"
):
async_create_issue(
hass,
HOMEASSISTANT_DOMAIN,
f"deprecated_yaml_{DOMAIN}",
breaks_in_ha_version="2024.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_yaml",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Lutron",
},
)
return
async_create_issue(
hass,
DOMAIN,
f"deprecated_yaml_import_issue_{result['reason']}",
breaks_in_ha_version="2024.7.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key=f"deprecated_yaml_import_issue_{result['reason']}",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Lutron",
},
)
async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
"""Set up the Lutron component."""
if DOMAIN in base_config:
hass.async_create_task(_async_import(hass, base_config))
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]
covers: list[tuple[str, Output]]
fans: list[tuple[str, Output]]
lights: list[tuple[str, Output]]
scenes: list[tuple[str, Keypad, Button, Led]]
switches: list[tuple[str, Output]]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up the Lutron integration."""
host = config_entry.data[CONF_HOST]
uid = config_entry.data[CONF_USERNAME]
pwd = config_entry.data[CONF_PASSWORD]
lutron_client = Lutron(host, uid, pwd)
await hass.async_add_executor_job(lutron_client.load_xml_db)
lutron_client.connect()
_LOGGER.info("Connected to main repeater at %s", host)
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)
entry_data = LutronData(
client=lutron_client,
binary_sensors=[],
buttons=[],
covers=[],
fans=[],
lights=[],
scenes=[],
switches=[],
)
# Sort our devices into types
_LOGGER.debug("Start adding devices")
for area in lutron_client.areas:
_LOGGER.debug("Working on area %s", area.name)
for output in area.outputs:
platform = None
_LOGGER.debug("Working on output %s", output.type)
if output.type == "SYSTEM_SHADE":
entry_data.covers.append((area.name, output))
platform = Platform.COVER
elif output.type == "CEILING_FAN_TYPE":
entry_data.fans.append((area.name, output))
platform = Platform.FAN
# Deprecated, should be removed in 2024.8
entry_data.lights.append((area.name, output))
elif output.is_dimmable:
entry_data.lights.append((area.name, output))
platform = Platform.LIGHT
else:
entry_data.switches.append((area.name, output))
platform = Platform.SWITCH
_async_check_entity_unique_id(
hass,
entity_registry,
platform,
output.uuid,
output.legacy_uuid,
entry_data.client.guid,
)
_async_check_device_identifiers(
hass,
device_registry,
output.uuid,
output.legacy_uuid,
entry_data.client.guid,
)
for keypad in area.keypads:
for button in keypad.buttons:
# If the button has a function assigned to it, add it as a scene
if button.name != "Unknown Button" and button.button_type in (
"SingleAction",
"Toggle",
"SingleSceneRaiseLower",
"MasterRaiseLower",
):
# Associate an LED with a button if there is one
led = next(
(led for led in keypad.leds if led.number == button.number),
None,
)
entry_data.scenes.append((area.name, keypad, button, led))
platform = Platform.SCENE
_async_check_entity_unique_id(
hass,
entity_registry,
platform,
button.uuid,
button.legacy_uuid,
entry_data.client.guid,
)
if led is not None:
platform = Platform.SWITCH
_async_check_entity_unique_id(
hass,
entity_registry,
platform,
led.uuid,
led.legacy_uuid,
entry_data.client.guid,
)
entry_data.buttons.append(LutronButton(hass, 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
_async_check_entity_unique_id(
hass,
entity_registry,
platform,
area.occupancy_group.uuid,
area.occupancy_group.legacy_uuid,
entry_data.client.guid,
)
_async_check_device_identifiers(
hass,
device_registry,
area.occupancy_group.uuid,
area.occupancy_group.legacy_uuid,
entry_data.client.guid,
)
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, lutron_client.guid)},
manufacturer="Lutron",
name="Main repeater",
)
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = entry_data
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
def _async_check_entity_unique_id(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
platform: str,
uuid: str,
legacy_uuid: str,
controller_guid: str,
) -> None:
"""If uuid becomes available update to use it."""
if not uuid:
return
unique_id = f"{controller_guid}_{legacy_uuid}"
entity_id = entity_registry.async_get_entity_id(
domain=platform, platform=DOMAIN, unique_id=unique_id
)
if entity_id:
new_unique_id = f"{controller_guid}_{uuid}"
_LOGGER.debug("Updating entity id from %s to %s", unique_id, new_unique_id)
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
def _async_check_device_identifiers(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
uuid: str,
legacy_uuid: str,
controller_guid: str,
) -> None:
"""If uuid becomes available update to use it."""
if not uuid:
return
unique_id = f"{controller_guid}_{legacy_uuid}"
device = device_registry.async_get_device(identifiers={(DOMAIN, unique_id)})
if device:
new_unique_id = f"{controller_guid}_{uuid}"
_LOGGER.debug("Updating device id from %s to %s", unique_id, new_unique_id)
device_registry.async_update_device(
device.id, new_identifiers={(DOMAIN, new_unique_id)}
)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Clean up resources and entities associated with the integration."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)