From 59959141af32055367aaaee99c23396a21a225c0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 26 Jun 2024 09:52:05 +0200 Subject: [PATCH] Remove Knocki triggers on runtime (#120452) * Bump Knocki to 0.2.0 * Remove triggers on runtime in Knocki * Fix --- homeassistant/components/knocki/__init__.py | 9 ++++- .../components/knocki/coordinator.py | 23 +++++++++++++ .../knocki/fixtures/more_triggers.json | 30 +++++++++++++++++ tests/components/knocki/test_event.py | 33 +++++++++++++++++-- 4 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/components/knocki/fixtures/more_triggers.json diff --git a/homeassistant/components/knocki/__init__.py b/homeassistant/components/knocki/__init__.py index ddf389649f2..42c3956bd68 100644 --- a/homeassistant/components/knocki/__init__.py +++ b/homeassistant/components/knocki/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from knocki import EventType, KnockiClient +from knocki import Event, EventType, KnockiClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, Platform @@ -30,6 +30,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bo client.register_listener(EventType.CREATED, coordinator.add_trigger) ) + async def _refresh_coordinator(_: Event) -> None: + await coordinator.async_refresh() + + entry.async_on_unload( + client.register_listener(EventType.DELETED, _refresh_coordinator) + ) + entry.runtime_data = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/knocki/coordinator.py b/homeassistant/components/knocki/coordinator.py index 020b3921a1e..f70fbdf79a7 100644 --- a/homeassistant/components/knocki/coordinator.py +++ b/homeassistant/components/knocki/coordinator.py @@ -2,7 +2,9 @@ from knocki import Event, KnockiClient, KnockiConnectionError, Trigger +from homeassistant.components.event import DOMAIN as EVENT_DOMAIN from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN, LOGGER @@ -19,12 +21,20 @@ class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): name=DOMAIN, ) self.client = client + self._known_triggers: set[tuple[str, int]] = set() async def _async_update_data(self) -> dict[int, Trigger]: try: triggers = await self.client.get_triggers() except KnockiConnectionError as exc: raise UpdateFailed from exc + current_triggers = { + (trigger.device_id, trigger.details.trigger_id) for trigger in triggers + } + removed_triggers = self._known_triggers - current_triggers + for trigger in removed_triggers: + await self._delete_device(trigger) + self._known_triggers = current_triggers return {trigger.details.trigger_id: trigger for trigger in triggers} def add_trigger(self, event: Event) -> None: @@ -32,3 +42,16 @@ class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): self.async_set_updated_data( {**self.data, event.payload.details.trigger_id: event.payload} ) + self._known_triggers.add( + (event.payload.device_id, event.payload.details.trigger_id) + ) + + async def _delete_device(self, trigger: tuple[str, int]) -> None: + """Delete a device from the coordinator.""" + device_id, trigger_id = trigger + entity_registry = er.async_get(self.hass) + entity_entry = entity_registry.async_get_entity_id( + EVENT_DOMAIN, DOMAIN, f"{device_id}_{trigger_id}" + ) + if entity_entry: + entity_registry.async_remove(entity_entry) diff --git a/tests/components/knocki/fixtures/more_triggers.json b/tests/components/knocki/fixtures/more_triggers.json new file mode 100644 index 00000000000..dbe4823e3d5 --- /dev/null +++ b/tests/components/knocki/fixtures/more_triggers.json @@ -0,0 +1,30 @@ +[ + { + "device": "KNC1-W-00000214", + "gesture": "d060b870-15ba-42c9-a932-2d2951087152", + "details": { + "description": "Eeee", + "name": "Aaaa", + "id": 31 + }, + "type": "homeassistant", + "user": "7a4d5bf9-01b1-413b-bb4d-77728e931dcc", + "updatedAt": 1716378013721, + "createdAt": 1716378013721, + "id": "1a050b25-7fed-4e0e-b5af-792b8b4650de" + }, + { + "device": "KNC1-W-00000214", + "gesture": "d060b870-15ba-42c9-a932-2d2951087152", + "details": { + "description": "Eeee", + "name": "Bbbb", + "id": 32 + }, + "type": "homeassistant", + "user": "7a4d5bf9-01b1-413b-bb4d-77728e931dcc", + "updatedAt": 1716378013721, + "createdAt": 1716378013721, + "id": "1a050b25-7fed-4e0e-b5af-792b8b4650de" + } +] diff --git a/tests/components/knocki/test_event.py b/tests/components/knocki/test_event.py index 4740ddc9167..4f639e08773 100644 --- a/tests/components/knocki/test_event.py +++ b/tests/components/knocki/test_event.py @@ -1,6 +1,6 @@ """Tests for the Knocki event platform.""" -from collections.abc import Callable +from collections.abc import Awaitable, Callable from unittest.mock import AsyncMock from knocki import Event, EventType, Trigger, TriggerDetails @@ -89,10 +89,39 @@ async def test_adding_runtime_entities( assert not hass.states.get("event.knc1_w_00000214_aaaa") add_trigger_function: Callable[[Event], None] = ( - mock_knocki_client.register_listener.call_args[0][1] + mock_knocki_client.register_listener.call_args_list[0][0][1] ) trigger = Trigger.from_dict(load_json_array_fixture("triggers.json", DOMAIN)[0]) add_trigger_function(Event(EventType.CREATED, trigger)) assert hass.states.get("event.knc1_w_00000214_aaaa") is not None + + +async def test_removing_runtime_entities( + hass: HomeAssistant, + mock_knocki_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test we can create devices on runtime.""" + mock_knocki_client.get_triggers.return_value = [ + Trigger.from_dict(trigger) + for trigger in load_json_array_fixture("more_triggers.json", DOMAIN) + ] + + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("event.knc1_w_00000214_aaaa") is not None + assert hass.states.get("event.knc1_w_00000214_bbbb") is not None + + remove_trigger_function: Callable[[Event], Awaitable[None]] = ( + mock_knocki_client.register_listener.call_args_list[1][0][1] + ) + trigger = Trigger.from_dict(load_json_array_fixture("triggers.json", DOMAIN)[0]) + + mock_knocki_client.get_triggers.return_value = [trigger] + + await remove_trigger_function(Event(EventType.DELETED, trigger)) + + assert hass.states.get("event.knc1_w_00000214_aaaa") is not None + assert hass.states.get("event.knc1_w_00000214_bbbb") is None