From 01c66aa7c1f9624095fe472ff02d9613423d7853 Mon Sep 17 00:00:00 2001 From: Kevin Addeman Date: Wed, 12 Oct 2022 20:26:54 -0400 Subject: [PATCH] Add support for area field from pylutron_caseta (#80221) --- .../components/lutron_caseta/__init__.py | 58 ++++++++++++------- .../components/lutron_caseta/binary_sensor.py | 10 +++- .../components/lutron_caseta/scene.py | 3 +- tests/components/lutron_caseta/__init__.py | 29 +++++++++- .../lutron_caseta/test_diagnostics.py | 18 +++++- 5 files changed, 88 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 5ef195514d3..dae83a045a2 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -177,7 +177,7 @@ async def async_setup_entry( ) buttons = bridge.buttons - _async_register_bridge_device(hass, entry_id, bridge_device) + _async_register_bridge_device(hass, entry_id, bridge_device, bridge) button_devices, device_info_by_device_id = _async_register_button_devices( hass, entry_id, bridge, bridge_device, buttons ) @@ -196,18 +196,25 @@ async def async_setup_entry( @callback def _async_register_bridge_device( - hass: HomeAssistant, config_entry_id: str, bridge_device: dict + hass: HomeAssistant, config_entry_id: str, bridge_device: dict, bridge: Smartbridge ) -> None: """Register the bridge device in the device registry.""" device_registry = dr.async_get(hass) - device_registry.async_get_or_create( - name=bridge_device["name"], - manufacturer=MANUFACTURER, - config_entry_id=config_entry_id, - identifiers={(DOMAIN, bridge_device["serial"])}, - model=f"{bridge_device['model']} ({bridge_device['type']})", - configuration_url="https://device-login.lutron.com", - ) + + device_args: DeviceInfo = { + "name": bridge_device["name"], + "manufacturer": MANUFACTURER, + "identifiers": {(DOMAIN, bridge_device["serial"])}, + "model": f"{bridge_device['model']} ({bridge_device['type']})", + "via_device": (DOMAIN, bridge_device["serial"]), + "configuration_url": "https://device-login.lutron.com", + } + + area = _area_name_from_id(bridge.areas, bridge_device["area"]) + if area != UNASSIGNED_AREA: + device_args["suggested_area"] = area + + device_registry.async_get_or_create(**device_args, config_entry_id=config_entry_id) @callback @@ -241,7 +248,10 @@ def _async_register_button_devices( continue seen.add(ha_device_serial) - area, name = _area_and_name_from_name(ha_device["name"]) + area = _area_name_from_id(bridge.areas, ha_device["area"]) + # name field is still a combination of area and name from pylytron-caseta + # extract the name portion only. + name = ha_device["name"].split("_")[-1] device_args: DeviceInfo = { "name": f"{area} {name}", "manufacturer": MANUFACTURER, @@ -265,12 +275,19 @@ def _handle_none_keypad_serial(keypad_device: dict, bridge_serial: int) -> str: return keypad_device["serial"] or f"{bridge_serial}_{keypad_device['device_id']}" -def _area_and_name_from_name(device_name: str) -> tuple[str, str]: - """Return the area and name from the devices internal name.""" - if "_" in device_name: - area_device_name = device_name.split("_", 1) - return area_device_name[0], area_device_name[1] - return UNASSIGNED_AREA, device_name +def _area_name_from_id(areas: dict[str, dict], area_id: str) -> str: + """Return the full area name including parent(s).""" + + if area_id is None: + return UNASSIGNED_AREA + + area = areas[area_id] + if "parent_id" in area: + parent_area = area["parent_id"] + if parent_area is not None: + return f"{_area_name_from_id(areas, parent_area)} {area['name']}" + + return area["name"] @callback @@ -316,7 +333,8 @@ def _async_subscribe_pico_remote_events( ) type_ = _lutron_model_to_device_type(ha_device["model"], ha_device["type"]) - area, name = _area_and_name_from_name(ha_device["name"]) + area = _area_name_from_id(bridge_device.areas, ha_device["area"]) + name = ha_device["name"].split("_")[-1] leap_button_number = device["button_number"] lip_button_number = async_get_lip_button(type_, leap_button_number) hass_device = dev_reg.async_get_device({(DOMAIN, ha_device_serial)}) @@ -377,8 +395,8 @@ class LutronCasetaDevice(Entity): if "parent_device" in device: # This is a child entity, handle the naming in button.py and switch.py return - - area, name = _area_and_name_from_name(device["name"]) + area = _area_name_from_id(self._smartbridge.areas, device["area"]) + name = device["name"].split("_")[-1] self._attr_name = full_name = f"{area} {name}" info = DeviceInfo( # Historically we used the device serial number for the identifier diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index 6df1125f7e9..29e59c426b5 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -6,13 +6,14 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_SUGGESTED_AREA from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntryType from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_and_name_from_name -from .const import CONFIG_URL, MANUFACTURER +from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice, _area_name_from_id +from .const import CONFIG_URL, MANUFACTURER, UNASSIGNED_AREA from .models import LutronCasetaData @@ -43,7 +44,8 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): def __init__(self, device, data): """Init an occupancy sensor.""" super().__init__(device, data) - _, name = _area_and_name_from_name(device["name"]) + area = _area_name_from_id(self._smartbridge.areas, device["area"]) + name = f"{area} {device['device_name']}" self._attr_name = name self._attr_device_info = DeviceInfo( identifiers={(CASETA_DOMAIN, self.unique_id)}, @@ -54,6 +56,8 @@ class LutronOccupancySensor(LutronCasetaDevice, BinarySensorEntity): configuration_url=CONFIG_URL, entry_type=DeviceEntryType.SERVICE, ) + if area != UNASSIGNED_AREA: + self._attr_device_info[ATTR_SUGGESTED_AREA] = area @property def is_on(self): diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index cc3be8a6479..997397c5b6c 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -9,7 +9,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import _area_and_name_from_name from .const import DOMAIN as CASETA_DOMAIN from .models import LutronCasetaData from .util import serial_to_unique_id @@ -42,7 +41,7 @@ class LutronCasetaScene(Scene): self._attr_device_info = DeviceInfo( identifiers={(CASETA_DOMAIN, data.bridge_device["serial"])}, ) - self._attr_name = _area_and_name_from_name(scene["name"])[1] + self._attr_name = scene["name"] self._attr_unique_id = f"scene_{bridge_unique_id}_{self._scene_id}" async def async_activate(self, **kwargs: Any) -> None: diff --git a/tests/components/lutron_caseta/__init__.py b/tests/components/lutron_caseta/__init__.py index f6af22034a7..cc6818dab14 100644 --- a/tests/components/lutron_caseta/__init__.py +++ b/tests/components/lutron_caseta/__init__.py @@ -105,7 +105,7 @@ class MockBridge: """Initialize MockBridge instance with configured mock connectivity.""" self.can_connect = can_connect self.is_currently_connected = False - self.areas = {} + self.areas = self.load_areas() self.occupancy_groups = {} self.scenes = self.get_scenes() self.devices = self.load_devices() @@ -126,10 +126,28 @@ class MockBridge: """Return whether the mock bridge is connected.""" return self.is_currently_connected + def load_areas(self): + """Loak mock areas into self.areas.""" + return { + "898": {"id": "898", "name": "Basement", "parent_id": None}, + "822": {"id": "822", "name": "Bedroom", "parent_id": "898"}, + "910": {"id": "910", "name": "Bathroom", "parent_id": "898"}, + "1024": {"id": "1024", "name": "Master Bedroom", "parent_id": None}, + "1025": {"id": "1025", "name": "Kitchen", "parent_id": None}, + "1026": {"id": "1026", "name": "Dining Room", "parent_id": None}, + "1205": {"id": "1205", "name": "Hallway", "parent_id": None}, + } + def load_devices(self): """Load mock devices into self.devices.""" return { - "1": {"serial": 1234, "name": "bridge", "model": "model", "type": "type"}, + "1": { + "serial": 1234, + "name": "bridge", + "model": "model", + "type": "type", + "area": "1205", + }, "801": { "device_id": "801", "current_state": 100, @@ -141,6 +159,7 @@ class MockBridge: "model": None, "serial": None, "tilt": None, + "area": "822", }, "802": { "device_id": "802", @@ -153,6 +172,7 @@ class MockBridge: "model": None, "serial": None, "tilt": None, + "area": "822", }, "803": { "device_id": "803", @@ -165,6 +185,7 @@ class MockBridge: "model": None, "serial": None, "tilt": None, + "area": "910", }, "804": { "device_id": "804", @@ -177,6 +198,7 @@ class MockBridge: "model": None, "serial": None, "tilt": None, + "area": "1024", }, "901": { "device_id": "901", @@ -189,6 +211,7 @@ class MockBridge: "model": None, "serial": 5442321, "tilt": None, + "area": "1025", }, "9": { "device_id": "9", @@ -203,7 +226,7 @@ class MockBridge: "model": "PJ2-3BRL-GXX-X01", "serial": 68551522, "device_name": "Pico", - "area": "6", + "area": "1026", }, "1355": { "device_id": "1355", diff --git a/tests/components/lutron_caseta/test_diagnostics.py b/tests/components/lutron_caseta/test_diagnostics.py index b0d6aae1058..8fa293c19d6 100644 --- a/tests/components/lutron_caseta/test_diagnostics.py +++ b/tests/components/lutron_caseta/test_diagnostics.py @@ -40,7 +40,15 @@ async def test_diagnostics(hass, hass_client) -> None: diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) assert diag == { "data": { - "areas": {}, + "areas": { + "898": {"id": "898", "name": "Basement", "parent_id": None}, + "822": {"id": "822", "name": "Bedroom", "parent_id": "898"}, + "910": {"id": "910", "name": "Bathroom", "parent_id": "898"}, + "1024": {"id": "1024", "name": "Master Bedroom", "parent_id": None}, + "1025": {"id": "1025", "name": "Kitchen", "parent_id": None}, + "1026": {"id": "1026", "name": "Dining Room", "parent_id": None}, + "1205": {"id": "1205", "name": "Hallway", "parent_id": None}, + }, "buttons": { "111": { "device_id": "111", @@ -73,6 +81,7 @@ async def test_diagnostics(hass, hass_client) -> None: "name": "bridge", "serial": 1234, "type": "type", + "area": "1205", }, "801": { "device_id": "801", @@ -85,6 +94,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": None, "tilt": None, + "area": "822", }, "802": { "device_id": "802", @@ -97,6 +107,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": None, "tilt": None, + "area": "822", }, "803": { "device_id": "803", @@ -109,6 +120,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": None, "tilt": None, + "area": "910", }, "804": { "device_id": "804", @@ -121,6 +133,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": None, "tilt": None, + "area": "1024", }, "901": { "device_id": "901", @@ -133,6 +146,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": None, "serial": 5442321, "tilt": None, + "area": "1025", }, "9": { "device_id": "9", @@ -147,7 +161,7 @@ async def test_diagnostics(hass, hass_client) -> None: "model": "PJ2-3BRL-GXX-X01", "serial": 68551522, "device_name": "Pico", - "area": "6", + "area": "1026", }, "1355": { "device_id": "1355",