diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py index 6b4ecdae026..064af41ac89 100644 --- a/homeassistant/components/tedee/coordinator.py +++ b/homeassistant/components/tedee/coordinator.py @@ -18,6 +18,7 @@ from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN @@ -50,7 +51,7 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]): ) self._next_get_locks = time.time() - self._current_locks: set[int] = set() + self._locks_last_update: set[int] = set() self.new_lock_callbacks: list[Callable[[int], None]] = [] @property @@ -84,15 +85,7 @@ 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) - + self._async_add_remove_locks() return self.tedee_client.locks_dict async def _async_update(self, update_fn: Callable[[], Awaitable[None]]) -> None: @@ -109,3 +102,32 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]): raise UpdateFailed("Error while updating data: %s" % str(ex)) from ex except (TedeeClientException, TimeoutError) as ex: raise UpdateFailed("Querying API failed. Error: %s" % str(ex)) from ex + + def _async_add_remove_locks(self) -> None: + """Add new locks, remove non-existing locks.""" + if not self._locks_last_update: + self._locks_last_update = set(self.tedee_client.locks_dict) + + if ( + current_locks := set(self.tedee_client.locks_dict) + ) == self._locks_last_update: + return + + # remove old locks + if removed_locks := self._locks_last_update - current_locks: + _LOGGER.debug("Removed locks: %s", ", ".join(map(str, removed_locks))) + device_registry = dr.async_get(self.hass) + for lock_id in removed_locks: + if device := device_registry.async_get_device( + identifiers={(DOMAIN, str(lock_id))} + ): + device_registry.async_remove_device(device.id) + + # add new locks + if new_locks := current_locks - self._locks_last_update: + _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) + + self._locks_last_update = current_locks diff --git a/homeassistant/components/tedee/entity.py b/homeassistant/components/tedee/entity.py index 6dfcbebe3de..ef75affebbc 100644 --- a/homeassistant/components/tedee/entity.py +++ b/homeassistant/components/tedee/entity.py @@ -38,9 +38,14 @@ class TedeeEntity(CoordinatorEntity[TedeeApiCoordinator]): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._lock = self.coordinator.data[self._lock.lock_id] + self._lock = self.coordinator.data.get(self._lock.lock_id, self._lock) super()._handle_coordinator_update() + @property + def available(self) -> bool: + """Return True if entity is available.""" + return super().available and self._lock.is_connected + class TedeeDescriptionEntity(TedeeEntity): """Base class for Tedee device entities.""" diff --git a/homeassistant/components/tedee/lock.py b/homeassistant/components/tedee/lock.py index 1025942d787..a01d13c3bbb 100644 --- a/homeassistant/components/tedee/lock.py +++ b/homeassistant/components/tedee/lock.py @@ -74,11 +74,6 @@ class TedeeLockEntity(TedeeEntity, LockEntity): """Return true if lock is jammed.""" return self._lock.is_state_jammed - @property - def available(self) -> bool: - """Return True if entity is available.""" - return super().available and self._lock.is_connected - async def async_unlock(self, **kwargs: Any) -> None: """Unlock the door.""" try: diff --git a/tests/components/tedee/test_lock.py b/tests/components/tedee/test_lock.py index 95a57078f56..fca1ae2b07f 100644 --- a/tests/components/tedee/test_lock.py +++ b/tests/components/tedee/test_lock.py @@ -25,7 +25,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed pytestmark = pytest.mark.usefixtures("init_integration") @@ -210,6 +210,36 @@ async def test_update_failed( assert state.state == STATE_UNAVAILABLE +async def test_cleanup_removed_locks( + hass: HomeAssistant, + mock_tedee: MagicMock, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Ensure removed locks are cleaned up.""" + + devices = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + + locks = [device.name for device in devices] + assert "Lock-1A2B" in locks + + # remove a lock and wait for coordinator + mock_tedee.locks_dict.pop(12345) + freezer.tick(timedelta(minutes=10)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + devices = dr.async_entries_for_config_entry( + device_registry, mock_config_entry.entry_id + ) + + locks = [device.name for device in devices] + assert "Lock-1A2B" not in locks + + async def test_new_lock( hass: HomeAssistant, mock_tedee: MagicMock,