diff --git a/homeassistant/components/knocki/__init__.py b/homeassistant/components/knocki/__init__.py index ef024d6f4d6..ddf389649f2 100644 --- a/homeassistant/components/knocki/__init__.py +++ b/homeassistant/components/knocki/__init__.py @@ -2,27 +2,18 @@ from __future__ import annotations -from dataclasses import dataclass - -from knocki import KnockiClient, KnockiConnectionError, Trigger +from knocki import EventType, KnockiClient from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession +from .coordinator import KnockiCoordinator + PLATFORMS: list[Platform] = [Platform.EVENT] -type KnockiConfigEntry = ConfigEntry[KnockiData] - - -@dataclass -class KnockiData: - """Knocki data.""" - - client: KnockiClient - triggers: list[Trigger] +type KnockiConfigEntry = ConfigEntry[KnockiCoordinator] async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bool: @@ -31,12 +22,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bo session=async_get_clientsession(hass), token=entry.data[CONF_TOKEN] ) - try: - triggers = await client.get_triggers() - except KnockiConnectionError as exc: - raise ConfigEntryNotReady from exc + coordinator = KnockiCoordinator(hass, client) - entry.runtime_data = KnockiData(client=client, triggers=triggers) + await coordinator.async_config_entry_first_refresh() + + entry.async_on_unload( + client.register_listener(EventType.CREATED, coordinator.add_trigger) + ) + + 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 new file mode 100644 index 00000000000..020b3921a1e --- /dev/null +++ b/homeassistant/components/knocki/coordinator.py @@ -0,0 +1,34 @@ +"""Update coordinator for Knocki integration.""" + +from knocki import Event, KnockiClient, KnockiConnectionError, Trigger + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, LOGGER + + +class KnockiCoordinator(DataUpdateCoordinator[dict[int, Trigger]]): + """The Knocki coordinator.""" + + def __init__(self, hass: HomeAssistant, client: KnockiClient) -> None: + """Initialize the coordinator.""" + super().__init__( + hass, + logger=LOGGER, + name=DOMAIN, + ) + self.client = client + + async def _async_update_data(self) -> dict[int, Trigger]: + try: + triggers = await self.client.get_triggers() + except KnockiConnectionError as exc: + raise UpdateFailed from exc + return {trigger.details.trigger_id: trigger for trigger in triggers} + + def add_trigger(self, event: Event) -> None: + """Add a trigger to the coordinator.""" + self.async_set_updated_data( + {**self.data, event.payload.details.trigger_id: event.payload} + ) diff --git a/homeassistant/components/knocki/event.py b/homeassistant/components/knocki/event.py index 8cd5de21958..adaf344e468 100644 --- a/homeassistant/components/knocki/event.py +++ b/homeassistant/components/knocki/event.py @@ -3,7 +3,7 @@ from knocki import Event, EventType, KnockiClient, Trigger from homeassistant.components.event import EventEntity -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -17,10 +17,26 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Set up Knocki from a config entry.""" - entry_data = entry.runtime_data + coordinator = entry.runtime_data + + added_triggers = set(coordinator.data) + + @callback + def _async_add_entities() -> None: + current_triggers = set(coordinator.data) + new_triggers = current_triggers - added_triggers + added_triggers.update(new_triggers) + if new_triggers: + async_add_entities( + KnockiTrigger(coordinator.data[trigger], coordinator.client) + for trigger in new_triggers + ) + + coordinator.async_add_listener(_async_add_entities) async_add_entities( - KnockiTrigger(trigger, entry_data.client) for trigger in entry_data.triggers + KnockiTrigger(trigger, coordinator.client) + for trigger in coordinator.data.values() ) diff --git a/tests/components/knocki/test_event.py b/tests/components/knocki/test_event.py index a53e2811854..4740ddc9167 100644 --- a/tests/components/knocki/test_event.py +++ b/tests/components/knocki/test_event.py @@ -7,13 +7,14 @@ from knocki import Event, EventType, Trigger, TriggerDetails import pytest from syrupy import SnapshotAssertion +from homeassistant.components.knocki.const import DOMAIN from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from . import setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, load_json_array_fixture, snapshot_platform async def test_entities( @@ -73,3 +74,25 @@ async def test_subscription( await hass.async_block_till_done() assert mock_knocki_client.register_listener.return_value.called + + +async def test_adding_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 = [] + + await setup_integration(hass, mock_config_entry) + + 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] + ) + 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