From 7385da626e0c5fd7996e99d2bd9acd6616cc7eee Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Sat, 6 Jan 2024 16:02:07 +0100 Subject: [PATCH] Add new locks automatically to tedee integration (#107372) * remove removed locks * move duplicated code to function * remove entities by removing device * add new locks automatically * add locks from coordinator * remove other PR stuff * add pullspring lock to test for coverage * requested changes --- .../components/tedee/binary_sensor.py | 11 +++++++ homeassistant/components/tedee/coordinator.py | 11 +++++++ homeassistant/components/tedee/lock.py | 9 ++++++ homeassistant/components/tedee/sensor.py | 11 +++++++ .../tedee/snapshots/test_binary_sensor.ambr | 2 +- .../components/tedee/snapshots/test_init.ambr | 2 +- tests/components/tedee/test_binary_sensor.py | 27 +++++++++++++++++ tests/components/tedee/test_lock.py | 29 +++++++++++++++++++ tests/components/tedee/test_sensor.py | 27 +++++++++++++++++ 9 files changed, 127 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tedee/binary_sensor.py b/homeassistant/components/tedee/binary_sensor.py index 9bb2cd0410c1..7efa25fa245c 100644 --- a/homeassistant/components/tedee/binary_sensor.py +++ b/homeassistant/components/tedee/binary_sensor.py @@ -66,6 +66,17 @@ async def async_setup_entry( ] ) + def _async_add_new_lock(lock_id: int) -> None: + lock = coordinator.data[lock_id] + async_add_entities( + [ + TedeeBinarySensorEntity(lock, coordinator, entity_description) + for entity_description in ENTITIES + ] + ) + + coordinator.new_lock_callbacks.append(_async_add_new_lock) + class TedeeBinarySensorEntity(TedeeDescriptionEntity, BinarySensorEntity): """Tedee sensor entity.""" diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py index 2b4f3c6d26b7..6b4ecdae0263 100644 --- a/homeassistant/components/tedee/coordinator.py +++ b/homeassistant/components/tedee/coordinator.py @@ -50,6 +50,8 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]): ) self._next_get_locks = time.time() + self._current_locks: set[int] = set() + self.new_lock_callbacks: list[Callable[[int], None]] = [] @property def bridge(self) -> TedeeBridge: @@ -82,6 +84,15 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]): ", ".join(map(str, self.tedee_client.locks_dict.keys())), ) + if not self._current_locks: + self._current_locks = set(self.tedee_client.locks_dict.keys()) + + if new_locks := set(self.tedee_client.locks_dict.keys()) - self._current_locks: + _LOGGER.debug("New locks found: %s", ", ".join(map(str, new_locks))) + for lock_id in new_locks: + for callback in self.new_lock_callbacks: + callback(lock_id) + return self.tedee_client.locks_dict async def _async_update(self, update_fn: Callable[[], Awaitable[None]]) -> None: diff --git a/homeassistant/components/tedee/lock.py b/homeassistant/components/tedee/lock.py index 751dfb446b72..1025942d7878 100644 --- a/homeassistant/components/tedee/lock.py +++ b/homeassistant/components/tedee/lock.py @@ -29,6 +29,15 @@ async def async_setup_entry( else: entities.append(TedeeLockEntity(lock, coordinator)) + def _async_add_new_lock(lock_id: int) -> None: + lock = coordinator.data[lock_id] + if lock.is_enabled_pullspring: + async_add_entities([TedeeLockWithLatchEntity(lock, coordinator)]) + else: + async_add_entities([TedeeLockEntity(lock, coordinator)]) + + coordinator.new_lock_callbacks.append(_async_add_new_lock) + async_add_entities(entities) diff --git a/homeassistant/components/tedee/sensor.py b/homeassistant/components/tedee/sensor.py index 9eb61e624c78..9880f73746dd 100644 --- a/homeassistant/components/tedee/sensor.py +++ b/homeassistant/components/tedee/sensor.py @@ -62,6 +62,17 @@ async def async_setup_entry( ] ) + def _async_add_new_lock(lock_id: int) -> None: + lock = coordinator.data[lock_id] + async_add_entities( + [ + TedeeSensorEntity(lock, coordinator, entity_description) + for entity_description in ENTITIES + ] + ) + + coordinator.new_lock_callbacks.append(_async_add_new_lock) + class TedeeSensorEntity(TedeeDescriptionEntity, SensorEntity): """Tedee sensor entity.""" diff --git a/tests/components/tedee/snapshots/test_binary_sensor.ambr b/tests/components/tedee/snapshots/test_binary_sensor.ambr index 16be8aafd0e7..a632ea3d57bb 100644 --- a/tests/components/tedee/snapshots/test_binary_sensor.ambr +++ b/tests/components/tedee/snapshots/test_binary_sensor.ambr @@ -128,4 +128,4 @@ 'last_updated': , 'state': 'off', }) -# --- +# --- \ No newline at end of file diff --git a/tests/components/tedee/snapshots/test_init.ambr b/tests/components/tedee/snapshots/test_init.ambr index e10a9f298bba..2a89b1fe7ef1 100644 --- a/tests/components/tedee/snapshots/test_init.ambr +++ b/tests/components/tedee/snapshots/test_init.ambr @@ -26,4 +26,4 @@ 'sw_version': None, 'via_device_id': None, }) -# --- +# --- \ No newline at end of file diff --git a/tests/components/tedee/test_binary_sensor.py b/tests/components/tedee/test_binary_sensor.py index bdb66c9c0a96..ee8c318d2ddf 100644 --- a/tests/components/tedee/test_binary_sensor.py +++ b/tests/components/tedee/test_binary_sensor.py @@ -1,13 +1,18 @@ """Tests for the Tedee Binary Sensors.""" +from datetime import timedelta from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory +from pytedee_async import TedeeLock import pytest from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from tests.common import async_fire_time_changed + pytestmark = pytest.mark.usefixtures("init_integration") BINARY_SENSORS = ( @@ -32,3 +37,25 @@ async def test_binary_sensors( entry = entity_registry.async_get(state.entity_id) assert entry assert entry == snapshot(name=f"entry-{key}") + + +async def test_new_binary_sensors( + hass: HomeAssistant, + mock_tedee: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure binary sensors for new lock are added automatically.""" + + for key in BINARY_SENSORS: + state = hass.states.get(f"binary_sensor.lock_4e5f_{key}") + assert state is None + + mock_tedee.locks_dict[666666] = TedeeLock("Lock-4E5F", 666666, 2) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + for key in BINARY_SENSORS: + state = hass.states.get(f"binary_sensor.lock_4e5f_{key}") + assert state diff --git a/tests/components/tedee/test_lock.py b/tests/components/tedee/test_lock.py index 995d036fba7b..95a57078f56d 100644 --- a/tests/components/tedee/test_lock.py +++ b/tests/components/tedee/test_lock.py @@ -3,6 +3,7 @@ from datetime import timedelta from unittest.mock import MagicMock from freezegun.api import FrozenDateTimeFactory +from pytedee_async import TedeeLock from pytedee_async.exception import ( TedeeClientException, TedeeDataUpdateException, @@ -207,3 +208,31 @@ async def test_update_failed( state = hass.states.get("lock.lock_1a2b") assert state is not None assert state.state == STATE_UNAVAILABLE + + +async def test_new_lock( + hass: HomeAssistant, + mock_tedee: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure new lock is added automatically.""" + + state = hass.states.get("lock.lock_4e5f") + assert state is None + + mock_tedee.locks_dict[666666] = TedeeLock("Lock-4E5F", 666666, 2) + mock_tedee.locks_dict[777777] = TedeeLock( + "Lock-6G7H", + 777777, + 4, + is_enabled_pullspring=True, + ) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("lock.lock_4e5f") + assert state + state = hass.states.get("lock.lock_6g7h") + assert state diff --git a/tests/components/tedee/test_sensor.py b/tests/components/tedee/test_sensor.py index 95cde20a82ff..274048082c08 100644 --- a/tests/components/tedee/test_sensor.py +++ b/tests/components/tedee/test_sensor.py @@ -1,14 +1,19 @@ """Tests for the Tedee Sensors.""" +from datetime import timedelta from unittest.mock import MagicMock +from freezegun.api import FrozenDateTimeFactory +from pytedee_async import TedeeLock import pytest from syrupy import SnapshotAssertion from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er +from tests.common import async_fire_time_changed + pytestmark = pytest.mark.usefixtures("init_integration") @@ -34,3 +39,25 @@ async def test_sensors( assert entry assert entry.device_id assert entry == snapshot(name=f"entry-{key}") + + +async def test_new_sensors( + hass: HomeAssistant, + mock_tedee: MagicMock, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure sensors for new lock are added automatically.""" + + for key in SENSORS: + state = hass.states.get(f"sensor.lock_4e5f_{key}") + assert state is None + + mock_tedee.locks_dict[666666] = TedeeLock("Lock-4E5F", 666666, 2) + + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + for key in SENSORS: + state = hass.states.get(f"sensor.lock_4e5f_{key}") + assert state