Add support for non-serialized devices (light, switch, cover, fan in RA3 Zones) (#75323)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Kevin Addeman 2022-08-20 16:56:19 -04:00 committed by GitHub
parent 87be71ce6a
commit 8b1713a691
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 355 additions and 20 deletions

View File

@ -630,8 +630,8 @@ build.json @home-assistant/supervisor
/tests/components/luftdaten/ @fabaff @frenck
/homeassistant/components/lupusec/ @majuss
/homeassistant/components/lutron/ @JonGilmore
/homeassistant/components/lutron_caseta/ @swails @bdraco
/tests/components/lutron_caseta/ @swails @bdraco
/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues
/tests/components/lutron_caseta/ @swails @bdraco @danaues
/homeassistant/components/lyric/ @timmo001
/tests/components/lyric/ @timmo001
/homeassistant/components/mastodon/ @fabaff

View File

@ -387,6 +387,13 @@ class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice):
self._device = self._smartbridge.get_device_by_id(self.device_id)
_LOGGER.debug(self._device)
@property
def unique_id(self):
"""Return a unique identifier if serial number is None."""
if self.serial is None:
return f"{self._bridge_unique_id}_{self.device_id}"
return super().unique_id
def _id_to_identifier(lutron_id: str) -> tuple[str, str]:
"""Convert a lutron caseta identifier to a device identifier."""

View File

@ -8,7 +8,7 @@
"homekit": {
"models": ["Smart Bridge"]
},
"codeowners": ["@swails", "@bdraco"],
"codeowners": ["@swails", "@bdraco", "@danaues"],
"iot_class": "local_push",
"loggers": ["pylutron_caseta"]
}

View File

@ -1,6 +1,103 @@
"""Tests for the Lutron Caseta integration."""
from unittest.mock import patch
from homeassistant.components.lutron_caseta import DOMAIN
from homeassistant.components.lutron_caseta.const import (
CONF_CA_CERTS,
CONF_CERTFILE,
CONF_KEYFILE,
)
from homeassistant.const import CONF_HOST
from tests.common import MockConfigEntry
ENTRY_MOCK_DATA = {
CONF_HOST: "1.1.1.1",
CONF_KEYFILE: "",
CONF_CERTFILE: "",
CONF_CA_CERTS: "",
}
_LEAP_DEVICE_TYPES = {
"light": [
"WallDimmer",
"PlugInDimmer",
"InLineDimmer",
"SunnataDimmer",
"TempInWallPaddleDimmer",
"WallDimmerWithPreset",
"Dimmed",
],
"switch": [
"WallSwitch",
"OutdoorPlugInSwitch",
"PlugInSwitch",
"InLineSwitch",
"PowPakSwitch",
"SunnataSwitch",
"TempInWallPaddleSwitch",
"Switched",
],
"fan": [
"CasetaFanSpeedController",
"MaestroFanSpeedController",
"FanSpeed",
],
"cover": [
"SerenaHoneycombShade",
"SerenaRollerShade",
"TriathlonHoneycombShade",
"TriathlonRollerShade",
"QsWirelessShade",
"QsWirelessHorizontalSheerBlind",
"QsWirelessWoodBlind",
"RightDrawDrape",
"Shade",
"SerenaTiltOnlyWoodBlind",
],
"sensor": [
"Pico1Button",
"Pico2Button",
"Pico2ButtonRaiseLower",
"Pico3Button",
"Pico3ButtonRaiseLower",
"Pico4Button",
"Pico4ButtonScene",
"Pico4ButtonZone",
"Pico4Button2Group",
"FourGroupRemote",
"SeeTouchTabletopKeypad",
"SunnataKeypad",
"SunnataKeypad_2Button",
"SunnataKeypad_3ButtonRaiseLower",
"SunnataKeypad_4Button",
"SeeTouchHybridKeypad",
"SeeTouchInternational",
"SeeTouchKeypad",
"HomeownerKeypad",
"GrafikTHybridKeypad",
"AlisseKeypad",
"PalladiomKeypad",
],
}
async def async_setup_integration(hass, mock_bridge) -> MockConfigEntry:
"""Set up a mock bridge."""
mock_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA)
mock_entry.add_to_hass(hass)
with patch(
"homeassistant.components.lutron_caseta.Smartbridge.create_tls"
) as create_tls:
create_tls.return_value = mock_bridge(can_connect=True)
await hass.config_entries.async_setup(mock_entry.entry_id)
await hass.async_block_till_done()
return mock_entry
class MockBridge:
"""Mock Lutron bridge that emulates configured connected status."""
@ -12,26 +109,120 @@ class MockBridge:
self.areas = {}
self.occupancy_groups = {}
self.scenes = self.get_scenes()
self.devices = self.get_devices()
self.devices = self.load_devices()
async def connect(self):
"""Connect the mock bridge."""
if self.can_connect:
self.is_currently_connected = True
def add_subscriber(self, device_id: str, callback_):
"""Mock a listener to be notified of state changes."""
def is_connected(self):
"""Return whether the mock bridge is connected."""
return self.is_currently_connected
def get_devices(self):
"""Return devices on the bridge."""
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"},
"801": {
"device_id": "801",
"current_state": 100,
"fan_speed": None,
"zone": "801",
"name": "Basement Bedroom_Main Lights",
"button_groups": None,
"type": "Dimmed",
"model": None,
"serial": None,
"tilt": None,
},
"802": {
"device_id": "802",
"current_state": 100,
"fan_speed": None,
"zone": "802",
"name": "Basement Bedroom_Left Shade",
"button_groups": None,
"type": "SerenaRollerShade",
"model": None,
"serial": None,
"tilt": None,
},
"803": {
"device_id": "803",
"current_state": 100,
"fan_speed": None,
"zone": "803",
"name": "Basement Bathroom_Exhaust Fan",
"button_groups": None,
"type": "Switched",
"model": None,
"serial": None,
"tilt": None,
},
"804": {
"device_id": "804",
"current_state": 100,
"fan_speed": None,
"zone": "804",
"name": "Master Bedroom_Ceiling Fan",
"button_groups": None,
"type": "FanSpeed",
"model": None,
"serial": None,
"tilt": None,
},
"901": {
"device_id": "901",
"current_state": 100,
"fan_speed": None,
"zone": "901",
"name": "Kitchen_Main Lights",
"button_groups": None,
"type": "WallDimmer",
"model": None,
"serial": 5442321,
"tilt": None,
},
}
def get_devices_by_domain(self, domain):
"""Return devices on the bridge."""
return {}
def get_devices(self) -> dict[str, dict]:
"""Will return all known devices connected to the Smart Bridge."""
return self.devices
def get_devices_by_domain(self, domain: str) -> list[dict]:
"""
Return a list of devices for the given domain.
:param domain: one of 'light', 'switch', 'cover', 'fan' or 'sensor'
:returns list of zero or more of the devices
"""
types = _LEAP_DEVICE_TYPES.get(domain, None)
# return immediately if not a supported domain
if types is None:
return []
return self.get_devices_by_types(types)
def get_devices_by_type(self, type_: str) -> list[dict]:
"""
Will return all devices of a given device type.
:param type_: LEAP device type, e.g. WallSwitch
"""
return [device for device in self.devices.values() if device["type"] == type_]
def get_devices_by_types(self, types: list[str]) -> list[dict]:
"""
Will return all devices for a list of given device types.
:param types: list of LEAP device types such as WallSwitch, WallDimmer
"""
return [device for device in self.devices.values() if device["type"] in types]
def get_scenes(self):
"""Return scenes on the bridge."""

View File

@ -20,7 +20,7 @@ from homeassistant.components.lutron_caseta.const import (
)
from homeassistant.const import CONF_HOST
from . import MockBridge
from . import ENTRY_MOCK_DATA, MockBridge
from tests.common import MockConfigEntry
@ -151,13 +151,7 @@ async def test_bridge_invalid_ssl_error(hass):
async def test_duplicate_bridge_import(hass):
"""Test that creating a bridge entry with a duplicate host errors."""
entry_mock_data = {
CONF_HOST: "1.1.1.1",
CONF_KEYFILE: "",
CONF_CERTFILE: "",
CONF_CA_CERTS: "",
}
mock_entry = MockConfigEntry(domain=DOMAIN, data=entry_mock_data)
mock_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_MOCK_DATA)
mock_entry.add_to_hass(hass)
with patch(
@ -168,7 +162,7 @@ async def test_duplicate_bridge_import(hass):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_IMPORT},
data=entry_mock_data,
data=ENTRY_MOCK_DATA,
)
assert result["type"] == data_entry_flow.FlowResultType.ABORT

View File

@ -0,0 +1,19 @@
"""Tests for the Lutron Caseta integration."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import MockBridge, async_setup_integration
async def test_cover_unique_id(hass: HomeAssistant) -> None:
"""Test a light unique id."""
await async_setup_integration(hass, MockBridge)
cover_entity_id = "cover.basement_bedroom_left_shade"
entity_registry = er.async_get(hass)
# Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID
assert entity_registry.async_get(cover_entity_id).unique_id == "000004d2_802"

View File

@ -48,7 +48,67 @@ async def test_diagnostics(hass, hass_client) -> None:
"name": "bridge",
"serial": 1234,
"type": "type",
}
},
"801": {
"device_id": "801",
"current_state": 100,
"fan_speed": None,
"zone": "801",
"name": "Basement Bedroom_Main Lights",
"button_groups": None,
"type": "Dimmed",
"model": None,
"serial": None,
"tilt": None,
},
"802": {
"device_id": "802",
"current_state": 100,
"fan_speed": None,
"zone": "802",
"name": "Basement Bedroom_Left Shade",
"button_groups": None,
"type": "SerenaRollerShade",
"model": None,
"serial": None,
"tilt": None,
},
"803": {
"device_id": "803",
"current_state": 100,
"fan_speed": None,
"zone": "803",
"name": "Basement Bathroom_Exhaust Fan",
"button_groups": None,
"type": "Switched",
"model": None,
"serial": None,
"tilt": None,
},
"804": {
"device_id": "804",
"current_state": 100,
"fan_speed": None,
"zone": "804",
"name": "Master Bedroom_Ceiling Fan",
"button_groups": None,
"type": "FanSpeed",
"model": None,
"serial": None,
"tilt": None,
},
"901": {
"device_id": "901",
"current_state": 100,
"fan_speed": None,
"zone": "901",
"name": "Kitchen_Main Lights",
"button_groups": None,
"type": "WallDimmer",
"model": None,
"serial": 5442321,
"tilt": None,
},
},
"occupancy_groups": {},
"scenes": {},

View File

@ -0,0 +1,19 @@
"""Tests for the Lutron Caseta integration."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import MockBridge, async_setup_integration
async def test_fan_unique_id(hass: HomeAssistant) -> None:
"""Test a light unique id."""
await async_setup_integration(hass, MockBridge)
fan_entity_id = "fan.master_bedroom_ceiling_fan"
entity_registry = er.async_get(hass)
# Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID
assert entity_registry.async_get(fan_entity_id).unique_id == "000004d2_804"

View File

@ -0,0 +1,27 @@
"""Tests for the Lutron Caseta integration."""
from homeassistant.const import STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import MockBridge, async_setup_integration
async def test_light_unique_id(hass: HomeAssistant) -> None:
"""Test a light unique id."""
await async_setup_integration(hass, MockBridge)
ra3_entity_id = "light.basement_bedroom_main_lights"
caseta_entity_id = "light.kitchen_main_lights"
entity_registry = er.async_get(hass)
# Assert that RA3 lights will have the bridge serial hash and the zone id as the uniqueID
assert entity_registry.async_get(ra3_entity_id).unique_id == "000004d2_801"
# Assert that Caseta lights will have the serial number as the uniqueID
assert entity_registry.async_get(caseta_entity_id).unique_id == "5442321"
state = hass.states.get(ra3_entity_id)
assert state.state == STATE_ON

View File

@ -0,0 +1,18 @@
"""Tests for the Lutron Caseta integration."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import MockBridge, async_setup_integration
async def test_switch_unique_id(hass: HomeAssistant) -> None:
"""Test a light unique id."""
await async_setup_integration(hass, MockBridge)
switch_entity_id = "switch.basement_bathroom_exhaust_fan"
entity_registry = er.async_get(hass)
# Assert that Caseta covers will have the bridge serial hash and the zone id as the uniqueID
assert entity_registry.async_get(switch_entity_id).unique_id == "000004d2_803"