mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 19:27:45 +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
|
/tests/components/luftdaten/ @fabaff @frenck
|
||||||
/homeassistant/components/lupusec/ @majuss
|
/homeassistant/components/lupusec/ @majuss
|
||||||
/homeassistant/components/lutron/ @JonGilmore
|
/homeassistant/components/lutron/ @JonGilmore
|
||||||
/homeassistant/components/lutron_caseta/ @swails @bdraco
|
/homeassistant/components/lutron_caseta/ @swails @bdraco @danaues
|
||||||
/tests/components/lutron_caseta/ @swails @bdraco
|
/tests/components/lutron_caseta/ @swails @bdraco @danaues
|
||||||
/homeassistant/components/lyric/ @timmo001
|
/homeassistant/components/lyric/ @timmo001
|
||||||
/tests/components/lyric/ @timmo001
|
/tests/components/lyric/ @timmo001
|
||||||
/homeassistant/components/mastodon/ @fabaff
|
/homeassistant/components/mastodon/ @fabaff
|
||||||
|
@ -387,6 +387,13 @@ class LutronCasetaDeviceUpdatableEntity(LutronCasetaDevice):
|
|||||||
self._device = self._smartbridge.get_device_by_id(self.device_id)
|
self._device = self._smartbridge.get_device_by_id(self.device_id)
|
||||||
_LOGGER.debug(self._device)
|
_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]:
|
def _id_to_identifier(lutron_id: str) -> tuple[str, str]:
|
||||||
"""Convert a lutron caseta identifier to a device identifier."""
|
"""Convert a lutron caseta identifier to a device identifier."""
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
"homekit": {
|
"homekit": {
|
||||||
"models": ["Smart Bridge"]
|
"models": ["Smart Bridge"]
|
||||||
},
|
},
|
||||||
"codeowners": ["@swails", "@bdraco"],
|
"codeowners": ["@swails", "@bdraco", "@danaues"],
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pylutron_caseta"]
|
"loggers": ["pylutron_caseta"]
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,103 @@
|
|||||||
"""Tests for the Lutron Caseta integration."""
|
"""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:
|
class MockBridge:
|
||||||
"""Mock Lutron bridge that emulates configured connected status."""
|
"""Mock Lutron bridge that emulates configured connected status."""
|
||||||
|
|
||||||
@ -12,26 +109,120 @@ class MockBridge:
|
|||||||
self.areas = {}
|
self.areas = {}
|
||||||
self.occupancy_groups = {}
|
self.occupancy_groups = {}
|
||||||
self.scenes = self.get_scenes()
|
self.scenes = self.get_scenes()
|
||||||
self.devices = self.get_devices()
|
self.devices = self.load_devices()
|
||||||
|
|
||||||
async def connect(self):
|
async def connect(self):
|
||||||
"""Connect the mock bridge."""
|
"""Connect the mock bridge."""
|
||||||
if self.can_connect:
|
if self.can_connect:
|
||||||
self.is_currently_connected = True
|
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):
|
def is_connected(self):
|
||||||
"""Return whether the mock bridge is connected."""
|
"""Return whether the mock bridge is connected."""
|
||||||
return self.is_currently_connected
|
return self.is_currently_connected
|
||||||
|
|
||||||
def get_devices(self):
|
def load_devices(self):
|
||||||
"""Return devices on the bridge."""
|
"""Load mock devices into self.devices."""
|
||||||
return {
|
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):
|
def get_devices(self) -> dict[str, dict]:
|
||||||
"""Return devices on the bridge."""
|
"""Will return all known devices connected to the Smart Bridge."""
|
||||||
return {}
|
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):
|
def get_scenes(self):
|
||||||
"""Return scenes on the bridge."""
|
"""Return scenes on the bridge."""
|
||||||
|
@ -20,7 +20,7 @@ from homeassistant.components.lutron_caseta.const import (
|
|||||||
)
|
)
|
||||||
from homeassistant.const import CONF_HOST
|
from homeassistant.const import CONF_HOST
|
||||||
|
|
||||||
from . import MockBridge
|
from . import ENTRY_MOCK_DATA, MockBridge
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -151,13 +151,7 @@ async def test_bridge_invalid_ssl_error(hass):
|
|||||||
async def test_duplicate_bridge_import(hass):
|
async def test_duplicate_bridge_import(hass):
|
||||||
"""Test that creating a bridge entry with a duplicate host errors."""
|
"""Test that creating a bridge entry with a duplicate host errors."""
|
||||||
|
|
||||||
entry_mock_data = {
|
mock_entry = MockConfigEntry(domain=DOMAIN, data=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.add_to_hass(hass)
|
mock_entry.add_to_hass(hass)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
@ -168,7 +162,7 @@ async def test_duplicate_bridge_import(hass):
|
|||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_IMPORT},
|
context={"source": config_entries.SOURCE_IMPORT},
|
||||||
data=entry_mock_data,
|
data=ENTRY_MOCK_DATA,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
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",
|
"name": "bridge",
|
||||||
"serial": 1234,
|
"serial": 1234,
|
||||||
"type": "type",
|
"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": {},
|
"occupancy_groups": {},
|
||||||
"scenes": {},
|
"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