mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
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:
parent
87be71ce6a
commit
8b1713a691
@ -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
|
||||
|
@ -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."""
|
||||
|
@ -8,7 +8,7 @@
|
||||
"homekit": {
|
||||
"models": ["Smart Bridge"]
|
||||
},
|
||||
"codeowners": ["@swails", "@bdraco"],
|
||||
"codeowners": ["@swails", "@bdraco", "@danaues"],
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pylutron_caseta"]
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
19
tests/components/lutron_caseta/test_cover.py
Normal file
19
tests/components/lutron_caseta/test_cover.py
Normal 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"
|
@ -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": {},
|
||||
|
19
tests/components/lutron_caseta/test_fan.py
Normal file
19
tests/components/lutron_caseta/test_fan.py
Normal 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"
|
27
tests/components/lutron_caseta/test_light.py
Normal file
27
tests/components/lutron_caseta/test_light.py
Normal 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
|
18
tests/components/lutron_caseta/test_switch.py
Normal file
18
tests/components/lutron_caseta/test_switch.py
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user