Replace device_id tuple with string in rfxtrx (#181436)

This commit is contained in:
Joakim Plate
2026-09-09 12:49:34 +02:00
committed by GitHub
parent 68e4e78de7
commit 7223c3836c
16 changed files with 263 additions and 205 deletions
+77 -13
View File
@@ -5,7 +5,7 @@ import binascii
from collections.abc import Callable, Mapping
import copy
import logging
from typing import Any, NamedTuple, cast
from typing import Any, NamedTuple, Self, cast
import RFXtrx as rfxtrxmod
@@ -58,6 +58,19 @@ class DeviceTuple(NamedTuple):
subtype: str
id_string: str
@classmethod
def from_unique_id(cls, unique_id: str) -> Self:
"""Construct a device tuple from a unique id."""
data = unique_id.split("_")
if len(data) != 3:
raise ValueError(f"Invalid device unique id: {unique_id}")
return cls(data[0], data[1], data[2])
@property
def unique_id(self) -> str:
"""Unique identifier of this device tuple."""
return f"{self.packettype}_{self.subtype}_{self.id_string}"
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -144,7 +157,7 @@ def _get_device_lookup(
for event_code, event_config in devices.items():
if (event := get_rfx_object(event_code)) is None:
continue
device_id = get_device_id(
device_id = get_device_tuple_from_device(
event.device, data_bits=event_config.get(CONF_DATA_BITS)
)
lookup[device_id] = event_config
@@ -189,7 +202,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None:
_LOGGER.debug("Receive RFXCOM event: %s", event_data)
data_bits = get_device_data_bits(event.device, devices)
device_id = get_device_id(event.device, data_bits=data_bits)
device_id = get_device_tuple_from_device(event.device, data_bits=data_bits)
if device_id not in devices:
if config[CONF_AUTOMATIC_ADD]:
@@ -202,7 +215,7 @@ async def async_setup_internal(hass: HomeAssistant, entry: ConfigEntry) -> None:
pt2262_devices.add(event.device.id_string)
device_entry = device_registry.async_get_device_by_identifier(
(DOMAIN, *device_id), # type: ignore[arg-type]
(DOMAIN, device_id.unique_id),
entry.entry_id,
)
if device_entry:
@@ -309,7 +322,7 @@ async def async_setup_platform_entry(
if not supported(event):
continue
device_id = get_device_id(
device_id = get_device_tuple_from_device(
event.device, data_bits=entity_info.get(CONF_DATA_BITS)
)
if device_id in device_ids:
@@ -339,6 +352,44 @@ async def async_setup_platform_entry(
)
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate an old config entry."""
version = entry.version
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
if version == 1:
# Convert from old tuple based device identifiers to standard string
device_registry = dr.async_get(hass)
for device_entry in dr.async_entries_for_config_entry(
device_registry, entry.entry_id
):
identifiers = set()
for identifier in device_entry.identifiers:
if identifier[0] == DOMAIN and len(cast(tuple, identifier)) == 4:
legacy_identifier = cast(tuple[str, str, str, str], identifier)
identifier = (
DOMAIN,
DeviceTuple(
packettype=legacy_identifier[1],
subtype=legacy_identifier[2],
id_string=legacy_identifier[3],
).unique_id,
)
identifiers.add(identifier)
device_registry.async_update_device(
device_entry.id, new_identifiers=identifiers
)
version = 2
hass.config_entries.async_update_entry(entry, version=version)
_LOGGER.debug(
"Migration to version %s.%s successful", entry.version, entry.minor_version
)
return True
def get_rfx_object(packetid: str) -> rfxtrxmod.RFXtrxEvent | None:
"""Return the RFXObject with the packetid."""
try:
@@ -384,7 +435,7 @@ def get_device_data_bits(
if device.packettype == DEVICE_PACKET_TYPE_LIGHTING4:
for device_id, entity_config in devices.items():
bits = entity_config.get(CONF_DATA_BITS)
if get_device_id(device, bits) == device_id:
if get_device_tuple_from_device(device, bits) == device_id:
data_bits = bits
break
return data_bits
@@ -419,7 +470,7 @@ def find_possible_pt2262_device(device_ids: set[str], device_id: str) -> str | N
return None
def get_device_id(
def get_device_tuple_from_device(
device: rfxtrxmod.RFXtrxDevice, data_bits: int | None = None
) -> DeviceTuple:
"""Calculate a device id for device."""
@@ -434,16 +485,29 @@ def get_device_id(
return DeviceTuple(f"{device.packettype:x}", f"{device.subtype:x}", id_string)
def get_device_tuples_from_identifiers(
identifiers: set[tuple[str, str]],
) -> list[DeviceTuple]:
"""Calculate the device tuples from a device entry."""
device_tuples = []
for identifier in identifiers:
if identifier[0] != DOMAIN:
continue
try:
device_tuples.append(DeviceTuple.from_unique_id(identifier[1]))
except ValueError as err:
_LOGGER.debug("%s", err)
return device_tuples
def get_device_tuple_from_identifiers(
identifiers: set[tuple[str, str]],
) -> DeviceTuple | None:
"""Calculate the device tuple from a device entry."""
identifier = next((x for x in identifiers if x[0] == DOMAIN and len(x) == 4), None)
if not identifier:
"""Calculate the first device tuple from a device entry."""
device_tuples = get_device_tuples_from_identifiers(identifiers)
if not device_tuples:
return None
# work around legacy identifier, being a multi tuple value
identifier2 = cast(tuple[str, str, str, str], identifier)
return DeviceTuple(identifier2[1], identifier2[2], identifier2[3])
return device_tuples[0]
async def async_remove_config_entry_device(
@@ -39,7 +39,7 @@ from homeassistant.helpers.typing import VolDictType
from . import (
DOMAIN,
DeviceTuple,
get_device_id,
get_device_tuple_from_device,
get_device_tuple_from_identifiers,
get_rfx_object,
)
@@ -180,7 +180,7 @@ class RfxtrxOptionsFlow(OptionsFlow):
if user_input is not None:
devices: dict[str, dict[str, Any] | None] = {}
device: dict[str, Any]
device_id = get_device_id(
device_id = get_device_tuple_from_device(
self._selected_device_object.device,
data_bits=user_input.get(CONF_DATA_BITS),
)
@@ -325,8 +325,8 @@ class RfxtrxOptionsFlow(OptionsFlow):
old_device_data = self._get_device_data(old_device)
new_device_data = self._get_device_data(replace_device)
old_device_id = "_".join(x for x in old_device_data[CONF_DEVICE_ID])
new_device_id = "_".join(x for x in new_device_data[CONF_DEVICE_ID])
old_device_id = old_device_data[CONF_DEVICE_ID].unique_id
new_device_id = new_device_data[CONF_DEVICE_ID].unique_id
entity_registry = er.async_get(self.hass)
entity_entries = er.async_entries_for_device(
@@ -414,12 +414,14 @@ class RfxtrxOptionsFlow(OptionsFlow):
def _can_add_device(self, new_rfx_obj: rfxtrxmod.RFXtrxEvent) -> bool:
"""Check if device does not already exist."""
new_device_id = get_device_id(new_rfx_obj.device)
new_device_id = get_device_tuple_from_device(new_rfx_obj.device)
for packet_id, entity_info in self.config_entry.data[CONF_DEVICES].items():
rfx_obj = get_rfx_object(packet_id)
assert rfx_obj
device_id = get_device_id(rfx_obj.device, entity_info.get(CONF_DATA_BITS))
device_id = get_device_tuple_from_device(
rfx_obj.device, entity_info.get(CONF_DATA_BITS)
)
if new_device_id == device_id:
return False
@@ -493,7 +495,7 @@ class RfxtrxOptionsFlow(OptionsFlow):
class RfxtrxConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for RFXCOM RFXtrx."""
VERSION = 1
VERSION = 2
@override
async def async_step_user(
+2 -10
View File
@@ -14,14 +14,6 @@ from . import DeviceTuple
from .const import ATTR_EVENT, COMMAND_GROUP_LIST, DATA_RFXOBJECT, DOMAIN, SIGNAL_EVENT
def _get_identifiers_from_device_tuple(
device_tuple: DeviceTuple,
) -> set[tuple[str, str]]:
"""Calculate the device identifier from a device tuple."""
# work around legacy identifier, being a multi tuple value
return {(DOMAIN, *device_tuple)} # type: ignore[arg-type]
class RfxtrxEntity(RestoreEntity):
"""Represents a Rfxtrx device.
@@ -42,11 +34,11 @@ class RfxtrxEntity(RestoreEntity):
) -> None:
"""Initialize the device."""
self._attr_device_info = DeviceInfo(
identifiers=_get_identifiers_from_device_tuple(device_id),
identifiers={(DOMAIN, device_id.unique_id)},
model=device.type_string,
name=f"{device.type_string} {device_id.id_string}",
)
self._attr_unique_id = "_".join(x for x in device_id)
self._attr_unique_id = device_id.unique_id
self._device = device
self._event = event
self._device_id = device_id
+1 -1
View File
@@ -289,7 +289,7 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity):
"""Initialize the sensor."""
super().__init__(device, device_id, event=event)
self.entity_description = entity_description
self._attr_unique_id = "_".join(x for x in (*device_id, entity_description.key))
self._attr_unique_id = f"{device_id.unique_id}_{entity_description.key}"
@override
async def async_added_to_hass(self) -> None:
+2
View File
@@ -1 +1,3 @@
"""Tests for the rfxtrx component."""
ENTRY_VERSION = 2
+20 -2
View File
@@ -13,6 +13,8 @@ from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.core import HomeAssistant
from homeassistant.util.dt import utcnow
from . import ENTRY_VERSION
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.light.conftest import mock_light_profiles # noqa: F401
@@ -37,6 +39,23 @@ def create_rfx_test_cfg(
}
def create_rfx_test_entry(
device="abcd",
automatic_add=False,
protocols=None,
devices=None,
host=None,
port=None,
):
"""Create rfxtrx config entry."""
entry_data = create_rfx_test_cfg(
device, automatic_add, protocols, devices, host, port
)
return MockConfigEntry(
domain="rfxtrx", unique_id=DOMAIN, data=entry_data, version=ENTRY_VERSION
)
async def setup_rfx_test_cfg(
hass: HomeAssistant,
device="abcd",
@@ -47,7 +66,7 @@ async def setup_rfx_test_cfg(
port=None,
):
"""Construct a rfxtrx config entry."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
device=device,
automatic_add=automatic_add,
devices=devices,
@@ -55,7 +74,6 @@ async def setup_rfx_test_cfg(
host=host,
port=port,
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.supports_remove_device = True
mock_entry.add_to_hass(hass)
+12 -27
View File
@@ -2,14 +2,13 @@
import pytest
from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.components.rfxtrx.const import ATTR_EVENT
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from .conftest import create_rfx_test_cfg
from .conftest import create_rfx_test_entry
from tests.common import MockConfigEntry, mock_restore_cache
from tests.common import mock_restore_cache
EVENT_SMOKE_DETECTOR_PANIC = "08200300a109000670"
EVENT_SMOKE_DETECTOR_NO_PANIC = "08200300a109000770"
@@ -25,9 +24,7 @@ EVENT_AC_118CDEA_2_ON = "0b1100100118cdea02010f70"
async def test_one(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 sensor."""
entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f230010f71": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0b1100cd0213c7f230010f71": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -41,7 +38,7 @@ async def test_one(hass: HomeAssistant, rfxtrx) -> None:
async def test_one_pt2262(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 PT2262 sensor."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0913000022670e013970": {
"data_bits": 4,
@@ -50,8 +47,6 @@ async def test_one_pt2262(hass: HomeAssistant, rfxtrx) -> None:
}
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -74,11 +69,9 @@ async def test_one_pt2262(hass: HomeAssistant, rfxtrx) -> None:
async def test_pt2262_unconfigured(hass: HomeAssistant, rfxtrx) -> None:
"""Test with discovery for PT2262."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={"0913000022670e013970": {}, "09130000226707013970": {}}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -107,9 +100,7 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state, event) -> None:
mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})])
entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f230010f71": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0b1100cd0213c7f230010f71": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -120,15 +111,13 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state, event) -> None:
async def test_several(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 3."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0b1100cd0213c7f230010f71": {},
"0b1100100118cdea02010f70": {},
"0b1100100118cdea03010f70": {},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -188,9 +177,9 @@ async def test_off_delay_restore(hass: HomeAssistant, rfxtrx) -> None:
],
)
entry_data = create_rfx_test_cfg(devices={EVENT_AC_118CDEA_2_ON: {"off_delay": 5}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(
devices={EVENT_AC_118CDEA_2_ON: {"off_delay": 5}}
)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -204,11 +193,9 @@ async def test_off_delay_restore(hass: HomeAssistant, rfxtrx) -> None:
async def test_off_delay(hass: HomeAssistant, rfxtrx, timestep) -> None:
"""Test with discovery."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={"0b1100100118cdea02010f70": {"off_delay": 5}}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -296,7 +283,7 @@ async def test_light(hass: HomeAssistant, rfxtrx_automatic) -> None:
async def test_pt2262_duplicate_id(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 sensor."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0913000022670e013970": {
"data_bits": 4,
@@ -310,8 +297,6 @@ async def test_pt2262_duplicate_id(hass: HomeAssistant, rfxtrx) -> None:
},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
+14 -4
View File
@@ -12,6 +12,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import ENTRY_VERSION
from tests.common import MockConfigEntry
SOME_PROTOCOLS = ["ac", "arc"]
@@ -285,6 +287,7 @@ async def test_options_global(hass: HomeAssistant) -> None:
"devices": {},
},
unique_id=DOMAIN,
version=ENTRY_VERSION,
)
with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
result = await start_options_flow(hass, entry)
@@ -320,6 +323,7 @@ async def test_no_protocols(hass: HomeAssistant) -> None:
"devices": {},
},
unique_id=DOMAIN,
version=ENTRY_VERSION,
)
with patch("homeassistant.components.rfxtrx.async_setup_entry", return_value=True):
result = await start_options_flow(hass, entry)
@@ -354,6 +358,7 @@ async def test_options_add_device(hass: HomeAssistant) -> None:
"devices": {},
},
unique_id=DOMAIN,
version=ENTRY_VERSION,
)
result = await start_options_flow(hass, entry)
@@ -416,6 +421,7 @@ async def test_options_add_duplicate_device(hass: HomeAssistant) -> None:
"devices": {"0b1100cd0213c7f230010f71": {}},
},
unique_id=DOMAIN,
version=ENTRY_VERSION,
)
entry.add_to_hass(hass)
@@ -458,6 +464,7 @@ async def test_options_replace_sensor_device(
},
},
unique_id=DOMAIN,
version=ENTRY_VERSION,
)
await start_options_flow(hass, entry)
@@ -508,7 +515,7 @@ async def test_options_replace_sensor_device(
(
elem.id
for elem in device_entries
if next(iter(elem.identifiers))[1:] == ("52", "1", "f0:04")
if next(iter(elem.identifiers))[1] == "52_1_f0:04"
),
None,
)
@@ -516,7 +523,7 @@ async def test_options_replace_sensor_device(
(
elem.id
for elem in device_entries
if next(iter(elem.identifiers))[1:] == ("52", "1", "23:04")
if next(iter(elem.identifiers))[1] == "52_1_23:04"
),
None,
)
@@ -620,6 +627,7 @@ async def test_options_replace_control_device(
},
},
unique_id=DOMAIN,
version=ENTRY_VERSION,
)
await start_options_flow(hass, entry)
@@ -642,7 +650,7 @@ async def test_options_replace_control_device(
(
elem.id
for elem in device_entries
if next(iter(elem.identifiers))[1:] == ("11", "0", "118cdea:2")
if next(iter(elem.identifiers))[1] == "11_0_118cdea:2"
),
None,
)
@@ -650,7 +658,7 @@ async def test_options_replace_control_device(
(
elem.id
for elem in device_entries
if next(iter(elem.identifiers))[1:] == ("11", "0", "1118cdea:2")
if next(iter(elem.identifiers))[1] == "11_0_1118cdea:2"
),
None,
)
@@ -715,6 +723,7 @@ async def test_options_add_and_configure_device(
"devices": {},
},
unique_id=DOMAIN,
version=ENTRY_VERSION,
)
result = await start_options_flow(hass, entry)
@@ -823,6 +832,7 @@ async def test_options_configure_rfy_cover_device(
"devices": {},
},
unique_id=DOMAIN,
version=ENTRY_VERSION,
)
result = await start_options_flow(hass, entry)
+7 -18
View File
@@ -4,20 +4,17 @@ from unittest.mock import call
import pytest
from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from .conftest import create_rfx_test_cfg
from .conftest import create_rfx_test_entry
from tests.common import MockConfigEntry, mock_restore_cache
from tests.common import mock_restore_cache
async def test_one_cover(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 cover."""
entry_data = create_rfx_test_cfg(devices={"0b1400cd0213c7f20d010f51": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0b1400cd0213c7f20d010f51": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -62,9 +59,7 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state) -> None:
mock_restore_cache(hass, [State(entity_id, state)])
entry_data = create_rfx_test_cfg(devices={"0b1400cd0213c7f20d010f51": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0b1400cd0213c7f20d010f51": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -75,15 +70,13 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state) -> None:
async def test_several_covers(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 3 covers."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0b1400cd0213c7f20d010f51": {},
"0A1400ADF394AB010D0060": {},
"09190000009ba8010100": {},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -122,14 +115,12 @@ async def test_discover_covers(hass: HomeAssistant, rfxtrx_automatic) -> None:
async def test_duplicate_cover(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 2 duplicate covers."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0b1400cd0213c7f20d010f51": {},
"0b1400cd0213c7f20d010f50": {},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -143,7 +134,7 @@ async def test_duplicate_cover(hass: HomeAssistant, rfxtrx) -> None:
async def test_rfy_cover(hass: HomeAssistant, rfxtrx) -> None:
"""Test Rfy venetian blind covers."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"071a000001020301": {
"venetian_blind_mode": "Unknown",
@@ -155,8 +146,6 @@ async def test_rfy_cover(hass: HomeAssistant, rfxtrx) -> None:
"0c1a0000010203030000000000": {"venetian_blind_mode": "EU"},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
+7 -27
View File
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from .conftest import create_rfx_test_cfg
from .conftest import create_rfx_test_entry
from tests.common import MockConfigEntry, async_get_device_automations
@@ -22,18 +22,14 @@ class DeviceTestData(NamedTuple):
"""Test data linked to a device."""
code: str
device_identifier: tuple[str, str, str, str]
device_identifier: tuple[str, str]
DEVICE_LIGHTING_1 = DeviceTestData("0710002a45050170", ("rfxtrx", "10", "0", "E5"))
DEVICE_LIGHTING_1 = DeviceTestData("0710002a45050170", ("rfxtrx", "10_0_E5"))
DEVICE_BLINDS_1 = DeviceTestData(
"09190000009ba8010100", ("rfxtrx", "19", "0", "009ba8:1")
)
DEVICE_BLINDS_1 = DeviceTestData("09190000009ba8010100", ("rfxtrx", "19_0_009ba8:1"))
DEVICE_TEMPHUM_1 = DeviceTestData(
"0a52080705020095220269", ("rfxtrx", "52", "8", "05:02")
)
DEVICE_TEMPHUM_1 = DeviceTestData("0a52080705020095220269", ("rfxtrx", "52_8_05:02"))
@pytest.mark.parametrize("device", [DEVICE_LIGHTING_1, DEVICE_TEMPHUM_1])
@@ -42,17 +38,13 @@ async def test_device_test_data(rfxtrx, device: DeviceTestData) -> None:
pkt: RFXtrx.lowlevel.Packet = RFXtrx.lowlevel.parse(bytearray.fromhex(device.code))
assert device.device_identifier == (
"rfxtrx",
f"{pkt.packettype:x}",
f"{pkt.subtype:x}",
pkt.id_string,
f"{pkt.packettype:x}_{pkt.subtype:x}_{pkt.id_string}",
)
async def setup_entry(hass: HomeAssistant, devices: dict[str, Any]) -> MockConfigEntry:
"""Construct a config setup."""
entry_data = create_rfx_test_cfg(devices=devices)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices=devices)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -95,18 +87,6 @@ async def test_get_actions(
)
assert device_entry
# Add alternate identifiers, to make sure we can handle future formats
identifiers: list[str] = list(*device_entry.identifiers)
device_registry.async_update_device(
device_entry.id,
new_identifiers=device_entry.identifiers
| {(identifiers[0], "_".join(identifiers[1:]))},
)
device_entry = device_registry.async_get_device_by_identifier(
device.device_identifier, mock_entry.entry_id
)
assert device_entry
actions = await async_get_device_automations(
hass, DeviceAutomationType.ACTION, device_entry.id
)
+6 -20
View File
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from .conftest import create_rfx_test_cfg
from .conftest import create_rfx_test_entry
from tests.common import (
MockConfigEntry,
@@ -25,20 +25,20 @@ class EventTestData(NamedTuple):
"""Test data linked to a device."""
code: str
device_identifier: tuple[str, str, str, str]
device_identifier: tuple[str, str]
type: str
subtype: str
DEVICE_LIGHTING_1 = ("rfxtrx", "10", "0", "E5")
DEVICE_LIGHTING_1 = ("rfxtrx", "10_0_E5")
EVENT_LIGHTING_1 = EventTestData("0710002a45050170", DEVICE_LIGHTING_1, "command", "On")
DEVICE_ROLLERTROL_1 = ("rfxtrx", "19", "0", "009ba8:1")
DEVICE_ROLLERTROL_1 = ("rfxtrx", "19_0_009ba8:1")
EVENT_ROLLERTROL_1 = EventTestData(
"09190000009ba8010100", DEVICE_ROLLERTROL_1, "command", "Down"
)
DEVICE_FIREALARM_1 = ("rfxtrx", "20", "3", "a10900:32")
DEVICE_FIREALARM_1 = ("rfxtrx", "20_3_a10900:32")
EVENT_FIREALARM_1 = EventTestData(
"08200300a109000670", DEVICE_FIREALARM_1, "status", "Panic"
)
@@ -46,9 +46,7 @@ EVENT_FIREALARM_1 = EventTestData(
async def setup_entry(hass: HomeAssistant, devices: dict[str, Any]) -> MockConfigEntry:
"""Construct a config setup."""
entry_data = create_rfx_test_cfg(devices=devices)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices=devices)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -93,18 +91,6 @@ async def test_get_triggers(
)
assert device_entry
# Add alternate identifiers, to make sure we can handle future formats
identifiers: list[str] = list(event.device_identifier)
device_registry.async_update_device(
device_entry.id,
new_identifiers=device_entry.identifiers
| {(identifiers[0], "_".join(identifiers[1:]))},
)
device_entry = device_registry.async_get_device_by_identifier(
event.device_identifier, mock_entry.entry_id
)
assert device_entry
expected_triggers = [
{
"domain": DOMAIN,
+78 -7
View File
@@ -4,7 +4,8 @@ from unittest.mock import ANY, call
import RFXtrx as rfxtrxmod
from homeassistant.components.rfxtrx.const import DOMAIN, EVENT_RFXTRX_EVENT
from homeassistant.components.rfxtrx import DOMAIN, DeviceTuple
from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
@@ -12,6 +13,7 @@ from homeassistant.setup import async_setup_component
from .conftest import setup_rfx_test_cfg
from tests.common import MockConfigEntry
from tests.typing import WebSocketGenerator
SOME_PROTOCOLS = ["ac", "arc"]
@@ -45,12 +47,12 @@ async def test_fire_event(
await rfxtrx.signal("0716000100900970")
device_id_1 = device_registry.async_get_device_by_identifier(
("rfxtrx", "11", "0", "213c7f2:16"), mock_entry.entry_id
("rfxtrx", "11_0_213c7f2:16"), mock_entry.entry_id
)
assert device_id_1
device_id_2 = device_registry.async_get_device_by_identifier(
("rfxtrx", "16", "0", "00:90"), mock_entry.entry_id
("rfxtrx", "16_0_00:90"), mock_entry.entry_id
)
assert device_id_2
@@ -97,16 +99,16 @@ async def test_ws_device_remove(
"""Test removing a device through device registry."""
assert await async_setup_component(hass, "config", {})
device_id = ["11", "0", "213c7f2:16"]
device_tuple = DeviceTuple("11", "0", "213c7f2:16")
mock_entry = await setup_rfx_test_cfg(
hass,
devices={
"0b1100cd0213c7f210010f51": {"fire_event": True, "device_id": device_id},
"0b1100cd0213c7f210010f51": {"fire_event": True, "device_id": device_tuple},
},
)
device_entry = device_registry.async_get_device_by_identifier(
("rfxtrx", *device_id), mock_entry.entry_id
("rfxtrx", device_tuple.unique_id), mock_entry.entry_id
)
assert device_entry
@@ -118,7 +120,7 @@ async def test_ws_device_remove(
# Verify device entry is removed
assert (
device_registry.async_get_device_by_identifier(
("rfxtrx", *device_id), mock_entry.entry_id
("rfxtrx", device_tuple.unique_id), mock_entry.entry_id
)
is None
)
@@ -214,3 +216,72 @@ async def test_reconnect(rfxtrx, hass: HomeAssistant) -> None:
assert config_entry.state is ConfigEntryState.LOADED
rfxtrx.connect.call_count = 2
async def test_migrate_entry(
hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
"""Test successful migration of entry data."""
legacy_config = {
"device": "abcd",
"host": None,
"port": None,
"automatic_add": True,
"protocols": [],
"devices": {
"0b1100cd0213c7f210010f51": {
"fire_event": True,
"device_id": ["11", "0", "213c7f2:16"],
},
"0716000100900970": {},
},
}
entry = MockConfigEntry(
domain=DOMAIN, unique_id=DOMAIN, data=legacy_config, version=1
)
entry.add_to_hass(hass)
device_1 = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={
(DOMAIN, "11", "0", "213c7f2:16"),
("dummy", "id"),
},
)
device_2 = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={
(DOMAIN, "16", "0", "00:90"),
},
)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
assert dict(entry.data) == {
"device": "abcd",
"host": None,
"port": None,
"automatic_add": True,
"protocols": [],
"devices": {
"0b1100cd0213c7f210010f51": {
"fire_event": True,
"device_id": ["11", "0", "213c7f2:16"],
},
"0716000100900970": {},
},
}
assert entry.version == 2
device_1 = device_registry.async_get(device_1.id)
assert device_1.identifiers == {
(DOMAIN, "11_0_213c7f2:16"),
("dummy", "id"),
}
device_2 = device_registry.async_get(device_2.id)
assert device_2.identifiers == {
(DOMAIN, "16_0_00:90"),
}
+5 -12
View File
@@ -5,20 +5,17 @@ from unittest.mock import call
import pytest
from homeassistant.components.light import ATTR_BRIGHTNESS
from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from .conftest import create_rfx_test_cfg
from .conftest import create_rfx_test_entry
from tests.common import MockConfigEntry, mock_restore_cache
from tests.common import mock_restore_cache
async def test_one_light(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 light."""
entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f210020f51": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0b1100cd0213c7f210020f51": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -102,9 +99,7 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state, brightness) ->
hass, [State(entity_id, state, attributes={ATTR_BRIGHTNESS: brightness})]
)
entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f210020f51": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0b1100cd0213c7f210020f51": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -116,15 +111,13 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state, brightness) ->
async def test_several_lights(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 3 lights."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0b1100cd0213c7f230020f71": {},
"0b1100100118cdea02020f70": {},
"0b1100101118cdea02050f70": {},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
+9 -24
View File
@@ -2,7 +2,6 @@
import pytest
from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.components.rfxtrx.const import ATTR_EVENT
from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
@@ -12,16 +11,14 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, State
from .conftest import create_rfx_test_cfg
from .conftest import create_rfx_test_entry
from tests.common import MockConfigEntry, mock_restore_cache
from tests.common import mock_restore_cache
async def test_default_config(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 0 sensor."""
entry_data = create_rfx_test_cfg(devices={})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -32,9 +29,7 @@ async def test_default_config(hass: HomeAssistant, rfxtrx) -> None:
async def test_one_sensor(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 sensor."""
entry_data = create_rfx_test_cfg(devices={"0a52080705020095220269": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0a52080705020095220269": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -61,9 +56,7 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state, event) -> None:
mock_restore_cache(hass, [State(entity_id, state, attributes={ATTR_EVENT: event})])
entry_data = create_rfx_test_cfg(devices={"0a520801070100b81b0279": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0a520801070100b81b0279": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -74,9 +67,7 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state, event) -> None:
async def test_one_sensor_no_datatype(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 sensor."""
entry_data = create_rfx_test_cfg(devices={"0a52080705020095220269": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0a52080705020095220269": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -121,14 +112,12 @@ async def test_one_sensor_no_datatype(hass: HomeAssistant, rfxtrx) -> None:
async def test_several_sensors(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 3 sensors."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0a52080705020095220269": {},
"0a520802060100ff0e0269": {},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -268,14 +257,12 @@ async def test_discover_sensor(hass: HomeAssistant, rfxtrx_automatic) -> None:
async def test_update_of_sensors(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 3 sensors."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0a52080705020095220269": {},
"0a520802060100ff0e0269": {},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -312,7 +299,7 @@ async def test_update_of_sensors(hass: HomeAssistant, rfxtrx) -> None:
async def test_rssi_sensor(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 sensor."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0913000022670e013b70": {
"data_bits": 4,
@@ -322,8 +309,6 @@ async def test_rssi_sensor(hass: HomeAssistant, rfxtrx) -> None:
"0b1100cd0213c7f230010f71": {},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
+5 -10
View File
@@ -2,21 +2,16 @@
from unittest.mock import call
from homeassistant.components.rfxtrx import DOMAIN
from homeassistant.core import HomeAssistant
from .conftest import create_rfx_test_cfg
from tests.common import MockConfigEntry
from .conftest import create_rfx_test_entry
async def test_one_chime(hass: HomeAssistant, rfxtrx, timestep) -> None:
"""Test with 1 entity."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={"0a16000000000000000000": {"off_delay": 2.0}}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
entity_id = "siren.byron_sx_00_00"
@@ -68,9 +63,9 @@ async def test_one_chime(hass: HomeAssistant, rfxtrx, timestep) -> None:
async def test_one_security1(hass: HomeAssistant, rfxtrx, timestep) -> None:
"""Test with 1 entity."""
entry_data = create_rfx_test_cfg(devices={"08200300a109000670": {"off_delay": 2.0}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(
devices={"08200300a109000670": {"off_delay": 2.0}}
)
mock_entry.add_to_hass(hass)
entity_id = "siren.kd101_smoke_detector_a10900_32"
+9 -23
View File
@@ -9,9 +9,9 @@ from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from .conftest import create_rfx_test_cfg
from .conftest import create_rfx_test_entry
from tests.common import MockConfigEntry, mock_restore_cache
from tests.common import mock_restore_cache
EVENT_RFY_ENABLE_SUN_AUTO = "0C1a0000030101011300000003"
EVENT_RFY_DISABLE_SUN_AUTO = "0C1a0000030101011400000003"
@@ -19,9 +19,7 @@ EVENT_RFY_DISABLE_SUN_AUTO = "0C1a0000030101011400000003"
async def test_one_switch(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 switch."""
entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f210010f51": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0b1100cd0213c7f210010f51": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -54,7 +52,7 @@ async def test_one_switch(hass: HomeAssistant, rfxtrx) -> None:
async def test_one_pt2262_switch(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 PT2262 switch."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0913000022670e013970": {
"data_bits": 4,
@@ -63,8 +61,6 @@ async def test_one_pt2262_switch(hass: HomeAssistant, rfxtrx) -> None:
}
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -103,9 +99,7 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state) -> None:
mock_restore_cache(hass, [State(entity_id, state)])
entry_data = create_rfx_test_cfg(devices={"0b1100cd0213c7f210010f51": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"0b1100cd0213c7f210010f51": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -116,15 +110,13 @@ async def test_state_restore(hass: HomeAssistant, rfxtrx, state) -> None:
async def test_several_switches(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 3 switches."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0b1100cd0213c7f230010f71": {},
"0b1100100118cdea02010f70": {},
"0b1100101118cdea02010f70": {},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -148,14 +140,12 @@ async def test_several_switches(hass: HomeAssistant, rfxtrx) -> None:
async def test_switch_events(hass: HomeAssistant, rfxtrx) -> None:
"""Event test with 2 switches."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0b1100cd0213c7f205010f51": {},
"0b1100cd0213c7f210010f51": {},
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -204,7 +194,7 @@ async def test_switch_events(hass: HomeAssistant, rfxtrx) -> None:
async def test_pt2262_switch_events(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 1 PT2262 switch."""
entry_data = create_rfx_test_cfg(
mock_entry = create_rfx_test_entry(
devices={
"0913000022670e013970": {
"data_bits": 4,
@@ -213,8 +203,6 @@ async def test_pt2262_switch_events(hass: HomeAssistant, rfxtrx) -> None:
}
}
)
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)
@@ -274,9 +262,7 @@ async def test_discover_rfy_sun_switch(hass: HomeAssistant, rfxtrx_automatic) ->
async def test_unknown_event_code(hass: HomeAssistant, rfxtrx) -> None:
"""Test with 3 switches."""
entry_data = create_rfx_test_cfg(devices={"1234567890": {}})
mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=DOMAIN, data=entry_data)
mock_entry = create_rfx_test_entry(devices={"1234567890": {}})
mock_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_entry.entry_id)