Improve handling of entity_id change in rflink (#183856)

This commit is contained in:
Erik Montnemery
2026-10-01 21:05:11 +02:00
committed by GitHub
parent ba0f70cec6
commit e2438a4b14
4 changed files with 151 additions and 24 deletions
+26 -18
View File
@@ -1,6 +1,7 @@
"""Support for Rflink devices."""
import asyncio
from collections import defaultdict
import logging
from typing import override
@@ -109,6 +110,23 @@ class RflinkDevice(Entity):
self._attr_available = availability
self.async_write_ha_state()
@callback
def _async_register_lookup(
self, lookup: defaultdict[str, list[str]], event_id: str
) -> None:
"""Route events for event_id to this entity until it is removed."""
entity_id = self.entity_id
lookup[event_id].append(entity_id)
@callback
def _async_unregister() -> None:
entity_ids = lookup[event_id]
entity_ids.remove(entity_id)
if not entity_ids:
del lookup[event_id]
self.async_on_remove(_async_unregister)
@override
async def async_added_to_hass(self) -> None:
"""Register update callback."""
@@ -124,34 +142,24 @@ class RflinkDevice(Entity):
].remove(tmp_entity)
# Register id and aliases
self.hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND][self._device_id].append(
self.entity_id
)
lookup = self.hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND]
group_lookup = self.hass.data[DATA_ENTITY_GROUP_LOOKUP][EVENT_KEY_COMMAND]
self._async_register_lookup(lookup, self._device_id)
if self._group:
self.hass.data[DATA_ENTITY_GROUP_LOOKUP][EVENT_KEY_COMMAND][
self._device_id
].append(self.entity_id)
self._async_register_lookup(group_lookup, self._device_id)
# aliases respond to both normal and group commands (allon/alloff)
if self._aliases:
for _id in self._aliases:
self.hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND][_id].append(
self.entity_id
)
self.hass.data[DATA_ENTITY_GROUP_LOOKUP][EVENT_KEY_COMMAND][_id].append(
self.entity_id
)
self._async_register_lookup(lookup, _id)
self._async_register_lookup(group_lookup, _id)
# group_aliases only respond to group commands (allon/alloff)
if self._group_aliases:
for _id in self._group_aliases:
self.hass.data[DATA_ENTITY_GROUP_LOOKUP][EVENT_KEY_COMMAND][_id].append(
self.entity_id
)
self._async_register_lookup(group_lookup, _id)
# nogroup_aliases only respond to normal commands
if self._nogroup_aliases:
for _id in self._nogroup_aliases:
self.hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND][_id].append(
self.entity_id
)
self._async_register_lookup(lookup, _id)
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_AVAILABILITY, self._availability_callback
+3 -6
View File
@@ -389,14 +389,11 @@ class RflinkSensor(RflinkDevice, SensorEntity):
].remove(tmp_entity)
# Register id and aliases
self.hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_SENSOR][self._device_id].append(
self.entity_id
)
lookup = self.hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_SENSOR]
self._async_register_lookup(lookup, self._device_id)
if self._aliases:
for _id in self._aliases:
self.hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_SENSOR][_id].append(
self.entity_id
)
self._async_register_lookup(lookup, _id)
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_AVAILABILITY, self._availability_callback
+51
View File
@@ -25,6 +25,7 @@ from homeassistant.const import (
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .test_init import mock_rflink
@@ -701,3 +702,53 @@ async def test_empty_discovery_info(
assert not hass.states.get(f"{DOMAIN}.protocol_0_0")
assert not hass.states.get("light.protocol_0_0")
assert "device_id not known and automatic add disabled" in caplog.text
async def test_removed_sensor_unregisters_lookups(
hass: HomeAssistant,
monkeypatch: pytest.MonkeyPatch,
entity_registry: er.EntityRegistry,
) -> None:
"""Test a removed sensor is dropped from the event lookups."""
config = {
"rflink": {
"port": "/dev/ttyABC0",
DOMAIN: {
"automatic_add": False,
"devices": {
"test_02": {
"name": "test_02",
"sensor_type": "humidity",
"aliases": ["test_alias_02_0"],
},
# Shares the alias of the removed sensor
"test_03": {
"name": "test_03",
"sensor_type": "humidity",
"aliases": ["test_alias_02_0"],
},
},
},
},
}
event_callback, _, _, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch)
lookup = hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_SENSOR]
assert lookup["test_02"] == ["sensor.test_02"]
entity_registry.async_remove("sensor.test_02")
await hass.async_block_till_done()
assert hass.states.get("sensor.test_02") is None
assert lookup == {
"test_03": ["sensor.test_03"],
"test_alias_02_0": ["sensor.test_03"],
}
for event_id in ("test_02", "test_alias_02_0"):
event_callback(
{"id": event_id, "sensor": "humidity", "value": 65, "unit": PERCENTAGE}
)
await hass.async_block_till_done()
assert hass.states.get("sensor.test_02") is None
assert hass.states.get("sensor.test_03").state == "65"
+71
View File
@@ -5,8 +5,15 @@ control of Rflink switch devices.
"""
from typing import Any
import pytest
from homeassistant.components.rflink import (
DATA_ENTITY_GROUP_LOOKUP,
DATA_ENTITY_LOOKUP,
EVENT_KEY_COMMAND,
)
from homeassistant.components.rflink.entity import EVENT_BUTTON_PRESSED
from homeassistant.const import (
ATTR_ENTITY_ID,
@@ -16,6 +23,7 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import CoreState, HomeAssistant, State, callback
from homeassistant.helpers import entity_registry as er
from .test_init import mock_rflink
@@ -599,3 +607,66 @@ async def test_restore_state(
assert state
assert state.state == STATE_OFF
assert state.attributes["assumed_state"]
@pytest.mark.parametrize(
"device_config",
[
pytest.param({}, id="device_id"),
pytest.param({"group": False}, id="nogroup_device_id"),
pytest.param({"aliases": ["test_alias_0_0"]}, id="alias"),
pytest.param({"group_aliases": ["test_group_0_0"]}, id="group_alias"),
pytest.param({"nogroup_aliases": ["test_nogroup_0_0"]}, id="nogroup_alias"),
],
)
async def test_removed_switch_unregisters_lookups(
hass: HomeAssistant,
monkeypatch: pytest.MonkeyPatch,
entity_registry: er.EntityRegistry,
device_config: dict[str, Any],
) -> None:
"""Test a removed switch is dropped from the event lookups."""
config = {
"rflink": {
"port": "/dev/ttyABC0",
DOMAIN: {
"devices": {
"protocol_0_0": {"name": "test", **device_config},
# Shares every alias id the removed switch may use
"protocol_0_1": {
"name": "other",
"aliases": ["test_alias_0_0"],
"group_aliases": ["test_group_0_0"],
"nogroup_aliases": ["test_nogroup_0_0"],
},
},
},
},
}
event_callback, _, _, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch)
lookup = hass.data[DATA_ENTITY_LOOKUP][EVENT_KEY_COMMAND]
group_lookup = hass.data[DATA_ENTITY_GROUP_LOOKUP][EVENT_KEY_COMMAND]
assert "switch.test" in lookup["protocol_0_0"]
entity_registry.async_remove("switch.test")
await hass.async_block_till_done()
assert hass.states.get("switch.test") is None
assert lookup == {
"protocol_0_1": ["switch.other"],
"test_alias_0_0": ["switch.other"],
"test_nogroup_0_0": ["switch.other"],
}
assert group_lookup == {
"protocol_0_1": ["switch.other"],
"test_alias_0_0": ["switch.other"],
"test_group_0_0": ["switch.other"],
}
for event_id in ("protocol_0_0", "test_alias_0_0", "test_nogroup_0_0"):
event_callback({"id": event_id, "command": "on"})
event_callback({"id": "test_group_0_0", "command": "allon"})
await hass.async_block_till_done()
assert hass.states.get("switch.test") is None
assert hass.states.get("switch.other").state == STATE_ON