From f8cb3f291b0a3bf6b3bf28e780ef0f2a92651ec5 Mon Sep 17 00:00:00 2001 From: imhotep Date: Sun, 9 Feb 2025 21:51:34 -0800 Subject: [PATCH 1/3] error handling for thumbnails --- custom_components/unifi_access/hub.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 63281d1..376d9cb 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -329,12 +329,15 @@ def _handle_location_update_v2(self, update): lock_rule ]["until"] if "thumbnail" in update["data"]: - existing_door.thumbnail = self._get_thumbnail_image( - f"{self.host}{STATIC_URL}{update['data']['thumbnail']['url']}" - ) - existing_door.thumbnail_last_updated = datetime.fromtimestamp( - update["data"]["thumbnail"]["door_thumbnail_last_update"] - ) + try: + existing_door.thumbnail = self._get_thumbnail_image( + f"{self.host}{STATIC_URL}{update['data']['thumbnail']['url']}" + ) + existing_door.thumbnail_last_updated = datetime.fromtimestamp( + update["data"]["thumbnail"]["door_thumbnail_last_update"] + ) + except (ApiError, ApiAuthError): + _LOGGER.error("Could not get thumbnail for door id %s", door_id) return existing_door def on_message(self, ws: websocket.WebSocketApp, message): From bd68f046cd3209269b57e7b9e7c949108414f0ca Mon Sep 17 00:00:00 2001 From: imhotep Date: Mon, 10 Feb 2025 21:21:12 -0800 Subject: [PATCH 2/3] fixing polling --- custom_components/unifi_access/coordinator.py | 2 +- custom_components/unifi_access/door.py | 18 ++++++++ custom_components/unifi_access/event.py | 20 ++++++--- custom_components/unifi_access/hub.py | 44 ++++++++++++------- custom_components/unifi_access/image.py | 11 +++-- custom_components/unifi_access/select.py | 2 +- 6 files changed, 67 insertions(+), 30 deletions(-) diff --git a/custom_components/unifi_access/coordinator.py b/custom_components/unifi_access/coordinator.py index 399a915..e36dcbc 100644 --- a/custom_components/unifi_access/coordinator.py +++ b/custom_components/unifi_access/coordinator.py @@ -27,7 +27,7 @@ def __init__(self, hass: HomeAssistant, hub) -> None: hass, _LOGGER, name="Unifi Access Coordinator", - always_update=False, + always_update=True, update_interval=update_interval, ) self.hub = hub diff --git a/custom_components/unifi_access/door.py b/custom_components/unifi_access/door.py index 502e2d4..46095dd 100644 --- a/custom_components/unifi_access/door.py +++ b/custom_components/unifi_access/door.py @@ -147,3 +147,21 @@ async def trigger_event(self, event: str, data: dict[str, str]): _LOGGER.info( "Event %s type %s for door %s fired", event, data["type"], self.name ) + + def __eq__(self, value) -> bool: + """Check if two doors are equal.""" + if isinstance(value, UnifiAccessDoor): + return ( + self.id == value.id + and self.name == value.name + and self.hub_type == value.hub_type + and self.door_position_status == value.door_position_status + and self.door_lock_relay_status == value.door_lock_relay_status + and self.lock_rule == value.lock_rule + and self.lock_rule_ended_time == value.lock_rule_ended_time + ) + return False + + def __repr__(self): + """Return string representation of door.""" + return f"" diff --git a/custom_components/unifi_access/event.py b/custom_components/unifi_access/event.py index bd26663..625a25a 100644 --- a/custom_components/unifi_access/event.py +++ b/custom_components/unifi_access/event.py @@ -18,6 +18,7 @@ DOORBELL_STOP_EVENT, ) from .door import UnifiAccessDoor +from .hub import UnifiAccessHub _LOGGER = logging.getLogger(__name__) @@ -28,15 +29,20 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add event entity for passed config entry.""" + hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] - coordinator = hass.data[DOMAIN]["coordinator"] + if hub.use_polling is False: + coordinator = hass.data[DOMAIN]["coordinator"] - async_add_entities( - (AccessEventEntity(hass, door) for door in coordinator.data.values()), - ) - async_add_entities( - (DoorbellPressedEventEntity(hass, door) for door in coordinator.data.values()), - ) + async_add_entities( + (AccessEventEntity(hass, door) for door in coordinator.data.values()), + ) + async_add_entities( + ( + DoorbellPressedEventEntity(hass, door) + for door in coordinator.data.values() + ), + ) class AccessEventEntity(EventEntity): diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 376d9cb..fdc2e24 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -119,12 +119,12 @@ def update(self): "Getting door updates from Unifi Access %s Use Polling %s. Doors? %s", self.host, self.use_polling, - self.doors, + self.doors.keys(), ) data = self._make_http_request(f"{self.host}{DOORS_URL}") for _i, door in enumerate(data): - if door["is_bind_hub"]: + if door["is_bind_hub"] is True: door_id = door["id"] door_lock_rule = {"type": "", "ended_time": 0} if self.supports_door_lock_rules: @@ -138,6 +138,16 @@ def update(self): ] existing_door.door_lock_rule = door_lock_rule["type"] existing_door.door_lock_ended_time = door_lock_rule["ended_time"] + _LOGGER.debug( + "Updated existing door, id: %s, name: %s, dps: %s, door_lock_relay_status: %s, door lock rule: %s, door lock rule ended time: %s using polling %s", + door_id, + door["name"], + door["door_position_status"], + door["door_lock_relay_status"], + door_lock_rule["type"], + door_lock_rule["ended_time"], + self.use_polling, + ) else: self._doors[door_id] = UnifiAccessDoor( door_id=door["id"], @@ -148,26 +158,26 @@ def update(self): door_lock_rule_ended_time=door_lock_rule["ended_time"], hub=self, ) + _LOGGER.debug( + "Found new door, id: %s, name: %s, dps: %s, door_lock_relay_status: %s, door lock rule: %s, door lock rule ended time: %s, using polling: %s", + door_id, + door["name"], + door["door_position_status"], + door["door_lock_relay_status"], + door_lock_rule["type"], + door_lock_rule["ended_time"], + self.use_polling, + ) + else: + _LOGGER.debug("Door %s is not bound to a hub. Ignoring", door) + if self.update_t is None and self.use_polling is False: + _LOGGER.debug("Starting continuous updates. Polling disabled") self.start_continuous_updates() + _LOGGER.debug("Got doors %s", self.doors) return self._doors - def update_door(self, door_id: int) -> None: - """Get latest door data for a specific door.""" - _LOGGER.info("Getting door update from Unifi Access with id %s", door_id) - updated_door = self._make_http_request(f"{self.host}{DOORS_URL}/{door_id}") - door_id = updated_door["id"] - _LOGGER.debug("got door update %s", updated_door) - if door_id in self.doors: - existing_door: UnifiAccessDoor = self.doors[door_id] - existing_door.door_lock_relay_status = updated_door[ - "door_lock_relay_status" - ] - existing_door.door_position_status = updated_door["door_position_status"] - existing_door.name = updated_door["name"] - _LOGGER.debug("door %s updated", door_id) - def authenticate(self, api_token: str) -> str: """Test if we can authenticate with the host.""" self.set_api_token(api_token) diff --git a/custom_components/unifi_access/image.py b/custom_components/unifi_access/image.py index 93e3762..cb4cadb 100644 --- a/custom_components/unifi_access/image.py +++ b/custom_components/unifi_access/image.py @@ -13,6 +13,7 @@ from .const import DOMAIN from .door import UnifiAccessDoor +from .hub import UnifiAccessHub _LOGGER = logging.getLogger(__name__) @@ -26,10 +27,12 @@ async def async_setup_entry( coordinator = hass.data[DOMAIN]["coordinator"] verify_ssl = config_entry.options.get("verify_ssl", False) - async_add_entities( - UnifiDoorImageEntity(hass, verify_ssl, door, config_entry.data["api_token"]) - for door in coordinator.data.values() - ) + hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] + if hub.use_polling is False: + async_add_entities( + UnifiDoorImageEntity(hass, verify_ssl, door, config_entry.data["api_token"]) + for door in coordinator.data.values() + ) class UnifiDoorImageEntity(ImageEntity): diff --git a/custom_components/unifi_access/select.py b/custom_components/unifi_access/select.py index d244b38..1a751b9 100644 --- a/custom_components/unifi_access/select.py +++ b/custom_components/unifi_access/select.py @@ -82,7 +82,7 @@ def _update_options(self): ): self._attr_options.remove("lock_early") else: - self._attr_options.add("lock_early") + self._attr_options.append("lock_early") async def async_select_option(self, option: str) -> None: "Select Door Lock Rule." From c023a4c3f868b8559f08f02c10619d9bb507ac0d Mon Sep 17 00:00:00 2001 From: imhotep Date: Tue, 11 Feb 2025 22:21:12 -0800 Subject: [PATCH 3/3] removing stale entities --- custom_components/unifi_access/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/custom_components/unifi_access/__init__.py b/custom_components/unifi_access/__init__.py index 2878e5d..47fa9f5 100644 --- a/custom_components/unifi_access/__init__.py +++ b/custom_components/unifi_access/__init__.py @@ -2,9 +2,12 @@ from __future__ import annotations +import logging + from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +import homeassistant.helpers.entity_registry as er from .const import DOMAIN from .coordinator import UnifiAccessCoordinator @@ -20,6 +23,22 @@ Platform.SENSOR, Platform.SWITCH, ] +_LOGGER = logging.getLogger(__name__) + + +async def remove_stale_entities(hass: HomeAssistant, entry_id: str): + """Remove entities that are stale.""" + _LOGGER.debug("Removing stale entities") + registry = er.async_get(hass) + config_entry_entities = registry.entities.get_entries_for_config_entry_id(entry_id) + stale_entities = [ + entity + for entity in config_entry_entities + if (entity.disabled or not hass.states.get(entity.entity_id)) + ] + for entity in stale_entities: + _LOGGER.debug("Removing stale entity: %s", entity.entity_id) + registry.async_remove(entity.entity_id) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -38,6 +57,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + await remove_stale_entities(hass, entry.entry_id) + return True