Remove stale devices for Comelit SimpleHome (#151519)

This commit is contained in:
Simone Chemelli
2025-09-18 20:43:38 +02:00
committed by GitHub
parent 4354214fbf
commit 21399818af
5 changed files with 243 additions and 36 deletions

View File

@@ -2,7 +2,7 @@
from abc import abstractmethod
from datetime import timedelta
from typing import TypeVar
from typing import Any, TypeVar
from aiocomelit.api import (
AlarmDataObject,
@@ -13,7 +13,16 @@ from aiocomelit.api import (
ComelitVedoAreaObject,
ComelitVedoZoneObject,
)
from aiocomelit.const import BRIDGE, VEDO
from aiocomelit.const import (
BRIDGE,
CLIMATE,
COVER,
IRRIGATION,
LIGHT,
OTHER,
SCENARIO,
VEDO,
)
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession
@@ -111,6 +120,32 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
async def _async_update_system_data(self) -> T:
"""Class method for updating data."""
async def _async_remove_stale_devices(
self,
previous_list: dict[int, Any],
current_list: dict[int, Any],
dev_type: str,
) -> None:
"""Remove stale devices."""
device_registry = dr.async_get(self.hass)
for i in previous_list:
if i not in current_list:
_LOGGER.debug(
"Detected change in %s devices: index %s removed",
dev_type,
i,
)
identifier = f"{self.config_entry.entry_id}-{dev_type}-{i}"
device = device_registry.async_get_device(
identifiers={(DOMAIN, identifier)}
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
class ComelitSerialBridge(
ComelitBaseCoordinator[dict[str, dict[int, ComelitSerialBridgeObject]]]
@@ -137,7 +172,15 @@ class ComelitSerialBridge(
self,
) -> dict[str, dict[int, ComelitSerialBridgeObject]]:
"""Specific method for updating data."""
return await self.api.get_all_devices()
data = await self.api.get_all_devices()
if self.data:
for dev_type in (CLIMATE, COVER, LIGHT, IRRIGATION, OTHER, SCENARIO):
await self._async_remove_stale_devices(
self.data[dev_type], data[dev_type], dev_type
)
return data
class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
@@ -163,4 +206,14 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
self,
) -> AlarmDataObject:
"""Specific method for updating data."""
return await self.api.get_all_areas_and_zones()
data = await self.api.get_all_areas_and_zones()
if self.data:
for obj_type in ("alarm_areas", "alarm_zones"):
await self._async_remove_stale_devices(
self.data[obj_type],
data[obj_type],
"area" if obj_type == "alarm_areas" else "zone",
)
return data

View File

@@ -72,9 +72,7 @@ rules:
repair-issues:
status: exempt
comment: no known use cases for repair issues or flows, yet
stale-devices:
status: todo
comment: missing implementation
stale-devices: done
# Platinum
async-dependency: done

View File

@@ -28,6 +28,18 @@ VEDO_PIN = 5678
FAKE_PIN = 0000
LIGHT0 = ComelitSerialBridgeObject(
index=0,
name="Light0",
status=0,
human_status="off",
type="light",
val=0,
protected=0,
zone="Bathroom",
power=0.0,
power_unit=WATT,
)
BRIDGE_DEVICE_QUERY = {
CLIMATE: {
0: ComelitSerialBridgeObject(
@@ -62,18 +74,7 @@ BRIDGE_DEVICE_QUERY = {
)
},
LIGHT: {
0: ComelitSerialBridgeObject(
index=0,
name="Light0",
status=0,
human_status="off",
type="light",
val=0,
protected=0,
zone="Bathroom",
power=0.0,
power_unit=WATT,
)
0: LIGHT0,
},
OTHER: {
0: ComelitSerialBridgeObject(
@@ -93,6 +94,13 @@ BRIDGE_DEVICE_QUERY = {
SCENARIO: {},
}
ZONE0 = ComelitVedoZoneObject(
index=0,
name="Zone0",
status_api="0x000",
status=0,
human_status=AlarmZoneState.REST,
)
VEDO_DEVICE_QUERY = AlarmDataObject(
alarm_areas={
0: ComelitVedoAreaObject(
@@ -112,12 +120,6 @@ VEDO_DEVICE_QUERY = AlarmDataObject(
)
},
alarm_zones={
0: ComelitVedoZoneObject(
index=0,
name="Zone0",
status_api="0x000",
status=0,
human_status=AlarmZoneState.REST,
)
0: ZONE0,
},
)

View File

@@ -2,8 +2,8 @@
from unittest.mock import AsyncMock
from aiocomelit.api import AlarmDataObject, ComelitVedoAreaObject, ComelitVedoZoneObject
from aiocomelit.const import AlarmAreaState, AlarmZoneState
from aiocomelit.api import AlarmDataObject, ComelitVedoAreaObject
from aiocomelit.const import AlarmAreaState
from freezegun.api import FrozenDateTimeFactory
import pytest
@@ -21,7 +21,7 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from . import setup_integration
from .const import VEDO_PIN
from .const import VEDO_PIN, ZONE0
from tests.common import MockConfigEntry, async_fire_time_changed
@@ -74,13 +74,7 @@ async def test_entity_availability(
)
},
alarm_zones={
0: ComelitVedoZoneObject(
index=0,
name="Zone0",
status_api="0x000",
status=0,
human_status=AlarmZoneState.REST,
)
0: ZONE0,
},
)

View File

@@ -2,6 +2,23 @@
from unittest.mock import AsyncMock
from aiocomelit.api import (
AlarmDataObject,
ComelitSerialBridgeObject,
ComelitVedoAreaObject,
ComelitVedoZoneObject,
)
from aiocomelit.const import (
CLIMATE,
COVER,
IRRIGATION,
LIGHT,
OTHER,
SCENARIO,
WATT,
AlarmAreaState,
AlarmZoneState,
)
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from freezegun.api import FrozenDateTimeFactory
import pytest
@@ -11,6 +28,7 @@ from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from . import setup_integration
from .const import LIGHT0, ZONE0
from tests.common import MockConfigEntry, async_fire_time_changed
@@ -47,3 +65,145 @@ async def test_coordinator_data_update_fails(
assert (state := hass.states.get(entity_id))
assert state.state == STATE_UNAVAILABLE
async def test_coordinator_stale_device_serial_bridge(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_serial_bridge: AsyncMock,
mock_serial_bridge_config_entry: MockConfigEntry,
) -> None:
"""Test coordinator data update removes stale Serial Brdige devices."""
entity_id_0 = "light.light0"
entity_id_1 = "light.light1"
mock_serial_bridge.get_all_devices.return_value = {
CLIMATE: {},
COVER: {},
LIGHT: {
0: LIGHT0,
1: ComelitSerialBridgeObject(
index=1,
name="Light1",
status=0,
human_status="off",
type="light",
val=0,
protected=0,
zone="Bathroom",
power=0.0,
power_unit=WATT,
),
},
OTHER: {},
IRRIGATION: {},
SCENARIO: {},
}
await setup_integration(hass, mock_serial_bridge_config_entry)
assert (state := hass.states.get(entity_id_0))
assert state.state == STATE_OFF
assert (state := hass.states.get(entity_id_1))
assert state.state == STATE_OFF
mock_serial_bridge.get_all_devices.return_value = {
CLIMATE: {},
COVER: {},
LIGHT: {0: LIGHT0},
OTHER: {},
IRRIGATION: {},
SCENARIO: {},
}
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id_0))
assert state.state == STATE_OFF
# Light1 is removed
assert not hass.states.get(entity_id_1)
async def test_coordinator_stale_device_vedo(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_vedo: AsyncMock,
mock_vedo_config_entry: MockConfigEntry,
) -> None:
"""Test coordinator data update removes stale VEDO devices."""
entity_id_0 = "sensor.zone0"
entity_id_1 = "sensor.zone1"
mock_vedo.get_all_areas_and_zones.return_value = AlarmDataObject(
alarm_areas={
0: ComelitVedoAreaObject(
index=0,
name="Area0",
p1=True,
p2=True,
ready=False,
armed=0,
alarm=False,
alarm_memory=False,
sabotage=False,
anomaly=False,
in_time=False,
out_time=False,
human_status=AlarmAreaState.DISARMED,
)
},
alarm_zones={
0: ZONE0,
1: ComelitVedoZoneObject(
index=1,
name="Zone1",
status_api="0x000",
status=0,
human_status=AlarmZoneState.REST,
),
},
)
await setup_integration(hass, mock_vedo_config_entry)
assert (state := hass.states.get(entity_id_0))
assert state.state == AlarmZoneState.REST.value
assert (state := hass.states.get(entity_id_1))
assert state.state == AlarmZoneState.REST.value
mock_vedo.get_all_areas_and_zones.return_value = AlarmDataObject(
alarm_areas={
0: ComelitVedoAreaObject(
index=0,
name="Area0",
p1=True,
p2=True,
ready=False,
armed=0,
alarm=False,
alarm_memory=False,
sabotage=False,
anomaly=False,
in_time=False,
out_time=False,
human_status=AlarmAreaState.DISARMED,
)
},
alarm_zones={
0: ZONE0,
},
)
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert (state := hass.states.get(entity_id_0))
assert state.state == AlarmZoneState.REST.value
# Zone1 is removed
assert not hass.states.get(entity_id_1)