From e63be0344b8e71e8b4b6a6a4c95ba64219a51552 Mon Sep 17 00:00:00 2001 From: Chris Giard Date: Thu, 25 Jul 2024 12:19:45 -1000 Subject: [PATCH 01/16] Initial add of UGT and UAH-Ent support --- custom_components/unifi_access/hub.py | 202 +++++++++++++++++++++----- 1 file changed, 168 insertions(+), 34 deletions(-) diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 0386a9c..75d4293 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -213,51 +213,185 @@ def on_message(self, ws: websocket.WebSocketApp, message): door_id, ) case "access.data.device.update": - door_id = update["data"]["door"]["unique_id"] - _LOGGER.info( - "Device Update via websocket %s", - door_id, - ) - if door_id in self.doors: - existing_door = self.doors[door_id] + device_type = update["data"]["device_type"] + if device_type == "UAH": + door_id = update["data"]["door"]["unique_id"] _LOGGER.info( - "Device update config for door %s", - existing_door.name, + "Device Update via websocket %s", + door_id, ) - try: - existing_door.door_position_status = ( - "close" - if next( - config["value"] - for config in update["data"]["configs"] - if config["key"] == "input_state_dps" - ) - == "on" - else "open" + if door_id in self.doors: + existing_door = self.doors[door_id] + _LOGGER.info( + "Device update config for door %s", + existing_door.name, ) - existing_door.door_lock_relay_status = ( - "unlock" - if next( - config["value"] - for config in update["data"]["configs"] - if config["key"] == "input_state_rly-lock_dry" + try: + existing_door.door_position_status = ( + "close" + if next( + config["value"] + for config in update["data"]["configs"] + if config["key"] == "input_state_dps" + ) + == "on" + else "open" ) - == "on" - else "lock" - ) + existing_door.door_lock_relay_status = ( + "unlock" + if next( + config["value"] + for config in update["data"]["configs"] + if config["key"] == "input_state_rly-lock_dry" + ) + == "on" + else "lock" + ) + _LOGGER.info( + "Door name %s with ID %s updated. Locked: %s DPS: %s", + existing_door.name, + door_id, + existing_door.door_lock_relay_status, + existing_door.door_position_status, + ) + except StopIteration: + _LOGGER.info( + "Ignoring update for door %s", + existing_door.name, + ) + elif device_type == "UGT": + # UGT has 2 ports + # Port 1 = vehicle gate, dps = data.config[input_gate_dps], relay = data.config[output_oper1_relay || output_oper2_relay] + # Port 2 = pedestrian gate, dps = data.config[input_door_dps], relay = data.config[output_door_lock_relay] + for ext in update["data"]["extensions"]: + door_id = ext["target_value"] + existing_door = self.doors[door_id] _LOGGER.info( - "Door name %s with ID %s updated. Locked: %s DPS: %s", + "Device update config for door %s", existing_door.name, - door_id, - existing_door.door_lock_relay_status, - existing_door.door_position_status, ) - except StopIteration: + + dev_id = ext["device_id"] + port = ext["source_id"] + if port == "port1": + try: + existing_door.door_position_status = ( + "close" + if next( + config["value"] + for config in update["data"]["configs"] + if config["key"] == "input_gate_dps" + ) + == "on" + else "open" + ) + existing_door.door_lock_relay_status = ( + "unlock" + if next( + config["value"] + for config in update["data"]["configs"] + if config["key"] == "output_oper1_relay" + ) + == "on" + else "lock" + ) + _LOGGER.info( + "Door name %s with ID %s updated. Locked: %s DPS: %s", + existing_door.name, + door_id, + existing_door.door_lock_relay_status, + existing_door.door_position_status, + ) + except StopIteration: + _LOGGER.info( + "Ignoring update for door %s", + existing_door.name, + ) + elif port == "port2": + try: + existing_door.door_position_status = ( + "close" + if next( + config["value"] + for config in update["data"]["configs"] + if config["key"] == "input_door_dps" + ) + == "on" + else "open" + ) + existing_door.door_lock_relay_status = ( + "unlock" + if next( + config["value"] + for config in update["data"]["configs"] + if config["key"] == "output_door_lock_relay" + ) + == "on" + else "lock" + ) + _LOGGER.info( + "Door name %s with ID %s updated. Locked: %s DPS: %s", + existing_door.name, + door_id, + existing_door.door_lock_relay_status, + existing_door.door_position_status, + ) + except StopIteration: + _LOGGER.info( + "Ignoring update for door %s", + existing_door.name, + ) + else: + pass # Raise exception + elif device_type == "UAH-Ent": + # UAH-Ent has 8 ports + # Port X dps = data.config[input_dX_dps], relay = data.config[output_dX_lock_relay] + for ext in update["data"]["extensions"]: + door_id = ext["target_value"] + existing_door = self.doors[door_id] _LOGGER.info( - "Ignoring update for door %s", + "Device update config for door %s", existing_door.name, ) + dev_id = ext["device_id"] + port = ext["source_id"].replace("port", "") + poskey = "input_d{}_dps".format(port) + relaykey = "output_d{}_lock_relay".format(port) + try: + existing_door.door_position_status = ( + "close" + if next( + config["value"] + for config in update["data"]["configs"] + if config["key"] == poskey + ) + == "on" + else "open" + ) + existing_door.door_lock_relay_status = ( + "unlock" + if next( + config["value"] + for config in update["data"]["configs"] + if config["key"] == relaykey + ) + == "on" + else "lock" + ) + _LOGGER.info( + "Door name %s with ID %s updated. Locked: %s DPS: %s", + existing_door.name, + door_id, + existing_door.door_lock_relay_status, + existing_door.door_position_status, + ) + except StopIteration: + _LOGGER.info( + "Ignoring update for door %s", + existing_door.name, + ) + case "access.remote_view": door_name = update["data"]["door_name"] _LOGGER.info("Doorbell Press %s", door_name) From 6fc714d18f5f0a9c2e7bbecc32c14c676c39e430 Mon Sep 17 00:00:00 2001 From: Chris Giard Date: Thu, 25 Jul 2024 13:08:23 -1000 Subject: [PATCH 02/16] Add code to handle multiple gate updates in a single message --- custom_components/unifi_access/hub.py | 36 +++++++++++++++++++-------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 75d4293..c97e581 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -186,8 +186,10 @@ def on_message(self, ws: websocket.WebSocketApp, message): event_attributes = None event_done_callback = None if "Hello" not in message: + _LOGGER.debug(f"Received message {message}") update = json.loads(message) existing_door = None + changed_doors = [] match update["event"]: case "access.dps_change": door_id = update["data"]["door_id"] @@ -201,6 +203,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): door_id, update["data"]["status"], ) + changed_doors.append(existing_door) case "access.data.device.remote_unlock": door_id = update["data"]["unique_id"] _LOGGER.info("Remote Unlock %s", door_id) @@ -212,6 +215,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): existing_door.name, door_id, ) + changed_doors.append(existing_door) case "access.data.device.update": device_type = update["data"]["device_type"] if device_type == "UAH": @@ -254,6 +258,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): existing_door.door_lock_relay_status, existing_door.door_position_status, ) + changed_doors.append(existing_door) except StopIteration: _LOGGER.info( "Ignoring update for door %s", @@ -302,6 +307,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): existing_door.door_lock_relay_status, existing_door.door_position_status, ) + changed_doors.append(existing_door) except StopIteration: _LOGGER.info( "Ignoring update for door %s", @@ -336,6 +342,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): existing_door.door_lock_relay_status, existing_door.door_position_status, ) + changed_doors.append(existing_door) except StopIteration: _LOGGER.info( "Ignoring update for door %s", @@ -386,6 +393,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): existing_door.door_lock_relay_status, existing_door.door_position_status, ) + changed_doors.append(existing_door) except StopIteration: _LOGGER.info( "Ignoring update for door %s", @@ -416,6 +424,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): door_name, update["data"]["request_id"], ) + changed_doors.append(existing_door) case "access.remote_view.change": doorbell_request_id = update["data"]["remote_call_request_id"] _LOGGER.info( @@ -442,6 +451,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): existing_door.name, doorbell_request_id, ) + changed_doors.append(existing_door) case "access.logs.add": door = next( ( @@ -481,6 +491,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): actor, access_type, ) + changed_doors.append(existing_door) case "access.hw.door_bell": door_id = update["data"]["door_id"] door_name = update["data"]["door_name"] @@ -521,17 +532,22 @@ def on_complete(_fut): door_name, update["data"]["request_id"], ) - if existing_door is not None: - asyncio.run_coroutine_threadsafe( - existing_door.publish_updates(), self.loop - ) - if event is not None and event_attributes is not None: - task = asyncio.run_coroutine_threadsafe( - existing_door.trigger_event(event, event_attributes), - self.loop, + changed_doors.append(existing_door) + if changed_doors: + for existing_door in changed_doors: + asyncio.run_coroutine_threadsafe( + existing_door.publish_updates(), self.loop ) - if event_done_callback is not None: - task.add_done_callback(event_done_callback) + # Doing this relies on the idea that a single message will only have one message_type + # and that a given message will only update events if a single door was updated. + # Refactor would be required if that doesn't hold true. + if event is not None and event_attributes is not None: + task = asyncio.run_coroutine_threadsafe( + existing_door.trigger_event(event, event_attributes), + self.loop, + ) + if event_done_callback is not None: + task.add_done_callback(event_done_callback) def on_error(self, ws: websocket.WebSocketApp, error): """Handle errors in the websocket client.""" From 9ea047057c006e67701bab0c9fd2988eaa513135 Mon Sep 17 00:00:00 2001 From: imhotep Date: Sun, 28 Jul 2024 17:06:17 -0700 Subject: [PATCH 03/16] adding support for evacuation/lock down and temporary lock rules --- custom_components/unifi_access/__init__.py | 11 +- .../unifi_access/binary_sensor.py | 2 - custom_components/unifi_access/const.py | 2 + custom_components/unifi_access/coordinator.py | 28 ++++ custom_components/unifi_access/door.py | 16 +++ custom_components/unifi_access/event.py | 1 - custom_components/unifi_access/hub.py | 118 ++++++++++++++-- custom_components/unifi_access/number.py | 72 ++++++++++ custom_components/unifi_access/select.py | 111 +++++++++++++++ custom_components/unifi_access/sensor.py | 70 ++++++++++ custom_components/unifi_access/switch.py | 127 ++++++++++++++++++ 11 files changed, 545 insertions(+), 13 deletions(-) create mode 100644 custom_components/unifi_access/number.py create mode 100644 custom_components/unifi_access/select.py create mode 100644 custom_components/unifi_access/sensor.py create mode 100644 custom_components/unifi_access/switch.py diff --git a/custom_components/unifi_access/__init__.py b/custom_components/unifi_access/__init__.py index cc20a98..c90e976 100644 --- a/custom_components/unifi_access/__init__.py +++ b/custom_components/unifi_access/__init__.py @@ -1,4 +1,5 @@ """The Unifi Access integration.""" + from __future__ import annotations from homeassistant.config_entries import ConfigEntry @@ -8,7 +9,15 @@ from .const import DOMAIN from .hub import UnifiAccessHub -PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.EVENT] +PLATFORMS: list[Platform] = [ + Platform.BINARY_SENSOR, + Platform.EVENT, + Platform.LOCK, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SWITCH, +] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/unifi_access/binary_sensor.py b/custom_components/unifi_access/binary_sensor.py index 9a68804..a6f6d31 100644 --- a/custom_components/unifi_access/binary_sensor.py +++ b/custom_components/unifi_access/binary_sensor.py @@ -49,7 +49,6 @@ def __init__(self, coordinator, door_id) -> None: """Initialize DPS Entity.""" super().__init__(coordinator, context=door_id) self._attr_device_class = BinarySensorDeviceClass.DOOR - self.id = door_id self.door = self.coordinator.data[door_id] self._attr_unique_id = self.door.id self.device_name = self.door.name @@ -97,7 +96,6 @@ def __init__(self, coordinator, door_id) -> None: """Initialize Doorbell Entity.""" super().__init__(coordinator, context=door_id) self._attr_device_class = BinarySensorDeviceClass.OCCUPANCY - self.id = door_id self.door = self.coordinator.data[door_id] self._attr_unique_id = f"doorbell_{self.door.id}" self.device_name = self.door.name diff --git a/custom_components/unifi_access/const.py b/custom_components/unifi_access/const.py index df16dff..76ffef8 100644 --- a/custom_components/unifi_access/const.py +++ b/custom_components/unifi_access/const.py @@ -6,7 +6,9 @@ UNIFI_ACCESS_API_PORT = 12445 DOORS_URL = "/api/v1/developer/doors" DOOR_UNLOCK_URL = "/api/v1/developer/doors/{door_id}/unlock" +DOOR_LOCK_RULE_URL = "/api/v1/developer/doors/{door_id}/lock_rule" DEVICE_NOTIFICATIONS_URL = "/api/v1/developer/devices/notifications" +DOORS_EMERGENCY_URL = "/api/v1/developer/doors/settings/emergency" DOORBELL_EVENT = "doorbell_press" DOORBELL_START_EVENT = "unifi_access_doorbell_start" diff --git a/custom_components/unifi_access/coordinator.py b/custom_components/unifi_access/coordinator.py index 343eaf7..9e9585f 100644 --- a/custom_components/unifi_access/coordinator.py +++ b/custom_components/unifi_access/coordinator.py @@ -40,3 +40,31 @@ async def _async_update_data(self): raise ConfigEntryAuthFailed from err except ApiError as err: raise UpdateFailed("Error communicating with API") from err + + +class UnifiAccessEvacuationAndLockdownSwitchCoordinator(DataUpdateCoordinator): + """Unifi Access Switch Coordinator.""" + + def __init__(self, hass: HomeAssistant, hub) -> None: + """Initialize Unifi Access Switch Coordinator.""" + update_interval = timedelta(seconds=3) if hub.use_polling is True else None + + super().__init__( + hass, + _LOGGER, + name="Unifi Access Evacuation and Lockdown Switch Coordinator", + update_interval=update_interval, + ) + self.hub = hub + + async def _async_update_data(self): + """Handle Unifi Access Switch Coordinator updates.""" + try: + async with asyncio.timeout(10): + return await self.hass.async_add_executor_job( + self.hub.get_doors_emergency_status + ) + except ApiAuthError as err: + raise ConfigEntryAuthFailed from err + except ApiError as err: + raise UpdateFailed("Error communicating with API") from err diff --git a/custom_components/unifi_access/door.py b/custom_components/unifi_access/door.py index 46e0288..ed44a6f 100644 --- a/custom_components/unifi_access/door.py +++ b/custom_components/unifi_access/door.py @@ -15,6 +15,8 @@ def __init__( name: str, door_position_status: str, door_lock_relay_status: str, + door_lock_rule: str, + door_lock_rule_ended_time: int, hub, ) -> None: """Initialize door.""" @@ -31,6 +33,9 @@ def __init__( self.door_position_status = door_position_status self.door_lock_relay_status = door_lock_relay_status self.doorbell_request_id = None + self.lock_rule = door_lock_rule + self.lock_rule_interval = 10 + self.lock_rule_ended_time = door_lock_rule_ended_time @property def doorbell_pressed(self) -> bool: @@ -76,6 +81,17 @@ def unlock(self) -> None: else: _LOGGER.error("Door with door ID %s is already unlocked", self.id) + def set_lock_rule(self, lock_rule_type) -> None: + """Set lock rule.""" + new_door_lock_rule = {"type": lock_rule_type} + if lock_rule_type == "custom": + new_door_lock_rule["interval"] = self.lock_rule_interval + self._hub.set_door_lock_rule(self._id, new_door_lock_rule) + + def get_lock_rule(self) -> None: + """Get lock rule.""" + self._hub.get_door_lock_rule(self._id) + def register_callback(self, callback: Callable[[], None]) -> None: """Register callback, called when Roller changes state.""" self._callbacks.add(callback) diff --git a/custom_components/unifi_access/event.py b/custom_components/unifi_access/event.py index 57d255e..8628965 100644 --- a/custom_components/unifi_access/event.py +++ b/custom_components/unifi_access/event.py @@ -92,7 +92,6 @@ class DoorbellPressedEventEntity(EventEntity): def __init__(self, hass: HomeAssistant, door) -> None: """Initialize Unifi Access Doorbell Event.""" self.hass = hass - self.id = door.id self.door: UnifiAccessDoor = door self._attr_unique_id = f"{self.door.id}_doorbell_press" self._attr_name = f"{self.door.name} Doorbell Press" diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 0386a9c..3a872f8 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -8,6 +8,7 @@ import logging import ssl from threading import Thread +from typing import Any, Literal, TypedDict, cast from urllib.parse import urlparse from requests import request @@ -18,9 +19,11 @@ from .const import ( ACCESS_EVENT, DEVICE_NOTIFICATIONS_URL, + DOOR_LOCK_RULE_URL, DOOR_UNLOCK_URL, DOORBELL_START_EVENT, DOORBELL_STOP_EVENT, + DOORS_EMERGENCY_URL, DOORS_URL, UNIFI_ACCESS_API_PORT, ) @@ -29,6 +32,28 @@ _LOGGER = logging.getLogger(__name__) +type EmergencyData = dict[str, bool] + + +class DoorLockRule(TypedDict): + """DoorLockRule. + + This class defines the different locking rules. + """ + + type: Literal["keep_lock", "keep_unlock", "custom", "reset", "lock_early"] + interval: int + + +class DoorLockRuleStatus(TypedDict): + """DoorLockRuleStatus. + + This class defines the active locking rule status. + """ + + type: Literal["schedule", "keep_lock", "keep_unlock", "custom", "lock_early"] + ended_time: int + class UnifiAccessHub: """UnifiAccessHub. @@ -67,6 +92,8 @@ def __init__( "Connection": "Upgrade", } self._doors: dict[str, UnifiAccessDoor] = {} + self.evacuation = False + self.lockdown = False self.update_t = None self.loop = asyncio.get_event_loop() @@ -92,17 +119,22 @@ def update(self): for _i, door in enumerate(data): door_id = door["id"] + door_lock_rule = self.get_door_lock_rule(door_id) if door_id in self.doors: existing_door = self.doors[door_id] existing_door.name = door["name"] existing_door.door_position_status = door["door_position_status"] existing_door.door_lock_relay_status = door["door_lock_relay_status"] + existing_door.door_lock_rule = door_lock_rule["type"] + existing_door.door_lock_ended_time = door_lock_rule["ended_time"] elif door["is_bind_hub"] is True: self._doors[door_id] = UnifiAccessDoor( door_id=door["id"], name=door["name"], door_position_status=door["door_position_status"], door_lock_relay_status=door["door_lock_relay_status"], + door_lock_rule=door_lock_rule["type"], + door_lock_rule_ended_time=door_lock_rule["ended_time"], hub=self, ) if self.update_t is None and self.use_polling is False: @@ -150,6 +182,45 @@ def authenticate(self, api_token: str) -> str: return "ok" + def get_door_lock_rule(self, door_id: str) -> DoorLockRule: + """Get door lock rule.""" + _LOGGER.debug("Getting door lock rule for door_id %s", door_id) + data = self._make_http_request( + f"{self.host}{DOOR_LOCK_RULE_URL}".format(door_id=door_id) + ) + _LOGGER.debug("Got door lock rule for door_id %s %s", door_id, data) + return cast(DoorLockRule, data) + + def set_door_lock_rule(self, door_id: str, door_lock_rule: DoorLockRule) -> None: + """Set door lock rule.""" + _LOGGER.info( + "Setting door lock rule for Door ID %s %s", door_id, door_lock_rule + ) + self._make_http_request( + f"{self.host}{DOOR_LOCK_RULE_URL}".format(door_id=door_id), + "PUT", + door_lock_rule, + ) + self.doors[door_id].lock_rule = door_lock_rule["type"] + if door_lock_rule["type"] == "custom": + self.doors[door_id].interval = door_lock_rule["interval"] + + def get_doors_emergency_status(self) -> EmergencyData: + """Get doors emergency status.""" + _LOGGER.debug("Getting doors emergency status") + data = self._make_http_request(f"{self.host}{DOORS_EMERGENCY_URL}") + self.evacuation = data["evacuation"] + self.lockdown = data["lockdown"] + _LOGGER.debug("Got doors emergency status %s", data) + return data + + def set_doors_emergency_status(self, emergency_data: EmergencyData) -> None: + """Set doors emergency status.""" + _LOGGER.info("Setting doors emergency status %s", emergency_data) + self._make_http_request( + f"{self.host}{DOORS_EMERGENCY_URL}", "PUT", emergency_data + ) + def unlock_door(self, door_id: str) -> None: """Test if we can authenticate with the host.""" _LOGGER.info("Unlocking door with id %s", door_id) @@ -157,13 +228,17 @@ def unlock_door(self, door_id: str) -> None: f"{self.host}{DOOR_UNLOCK_URL}".format(door_id=door_id), "PUT" ) - def _make_http_request(self, url, method="GET") -> dict: + def _make_http_request(self, url, method="GET", data=None) -> dict: """Make HTTP request to Unifi Access API server.""" + _LOGGER.debug( + "Making HTTP %s Request with URL %s and data %s", method, url, data + ) r = request( method, url, headers=self._http_headers, verify=self.verify_ssl, + json=data, timeout=10, ) @@ -175,6 +250,8 @@ def _make_http_request(self, url, method="GET") -> dict: response = r.json() + _LOGGER.debug("HTTP Response %s", response) + return response["data"] def on_message(self, ws: websocket.WebSocketApp, message): @@ -186,6 +263,7 @@ def on_message(self, ws: websocket.WebSocketApp, message): event_attributes = None event_done_callback = None if "Hello" not in message: + # _LOGGER.debug("Websocket Message %s", message) update = json.loads(message) existing_door = None match update["event"]: @@ -245,19 +323,39 @@ def on_message(self, ws: websocket.WebSocketApp, message): == "on" else "lock" ) + except StopIteration: _LOGGER.info( - "Door name %s with ID %s updated. Locked: %s DPS: %s", + "No DPS Config for %s, trying lock rules", existing_door.name, - door_id, - existing_door.door_lock_relay_status, - existing_door.door_position_status, ) - except StopIteration: + else: + existing_door.lock_rule = next( + ( + config["value"] + for config in update["data"]["configs"] + # if config["key"] == "temp_lock_type" + if config["key"] == "lock_type" + ), + "", + ) + existing_door.lock_rule_ended_time = next( + ( + config["value"] + for config in update["data"]["configs"] + if config["key"] == "lock_end_time" + ), + 0, + ) + finally: _LOGGER.info( - "Ignoring update for door %s", + "Door name %s with ID %s updated. Locked: %s DPS: %s, Lock Rule: %s, Lock Rule Ended Time %s", existing_door.name, + door_id, + existing_door.door_lock_relay_status, + existing_door.door_position_status, + existing_door.lock_rule, + existing_door.lock_rule_ended_time, ) - case "access.remote_view": door_name = update["data"]["door_name"] _LOGGER.info("Doorbell Press %s", door_name) @@ -387,6 +485,8 @@ def on_complete(_fut): door_name, update["data"]["request_id"], ) + case _: + _LOGGER.info("Got unhandled websocket message %s", update["event"]) if existing_door is not None: asyncio.run_coroutine_threadsafe( existing_door.publish_updates(), self.loop @@ -414,7 +514,7 @@ def on_close(self, ws: websocket.WebSocketApp, close_status_code, close_msg): close_status_code, close_msg, ) - sslopt = None + sslopt: dict[Any, Any] if self.verify_ssl is False: sslopt = {"cert_reqs": ssl.CERT_NONE} ws.run_forever(sslopt=sslopt, reconnect=5) diff --git a/custom_components/unifi_access/number.py b/custom_components/unifi_access/number.py new file mode 100644 index 0000000..0dd5725 --- /dev/null +++ b/custom_components/unifi_access/number.py @@ -0,0 +1,72 @@ +"""Platform for number (interval) integration.""" + +import logging + +from homeassistant.components.number import RestoreNumber +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import UnifiAccessCoordinator +from .door import UnifiAccessDoor +from .hub import UnifiAccessHub + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Select entity for passed config entry.""" + hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] + + coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) + + await coordinator.async_config_entry_first_refresh() + + async_add_entities( + [ + TemporaryLockRuleIntervalNumberEntity(door) + for door in coordinator.data.values() + ] + ) + + +class TemporaryLockRuleIntervalNumberEntity(RestoreNumber): + """Unifi Access Temporary Lock Rule Interval Interval.""" + + def __init__(self, door: UnifiAccessDoor) -> None: + """Initialize Unifi Access Door Lock Rule Interval.""" + super().__init__() + self.door: UnifiAccessDoor = door + self._attr_unique_id = f"door_lock_rule_interval_{self.door.id}" + self._attr_name = f"{self.door.name} Door Lock Rule Interval (min)" + self._attr_native_value = 10 + self._attr_native_min_value = 1 + self._attr_native_max_value = 480 + + @property + def device_info(self) -> DeviceInfo: + """Get Unifi Access Door Lock device information.""" + return DeviceInfo( + identifiers={(DOMAIN, self.door.id)}, + name=self.door.name, + model="UAH", + manufacturer="Unifi", + ) + + async def async_added_to_hass(self) -> None: + """Add Unifi Access Door Lock Rule Interval to Home Assistant.""" + await super().async_added_to_hass() + await self.async_get_last_number_data() + if self.native_value: + self.door.lock_rule_interval = int(self.native_value) + + def set_native_value(self, value: float) -> None: + "Select Door Lock Rule Interval (in minutes)." + self._attr_native_value = value + self.door.lock_rule_interval = int(value) diff --git a/custom_components/unifi_access/select.py b/custom_components/unifi_access/select.py new file mode 100644 index 0000000..b1b033b --- /dev/null +++ b/custom_components/unifi_access/select.py @@ -0,0 +1,111 @@ +"""Platform for select integration.""" + +import logging + +from homeassistant.components.select import SelectEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import UnifiAccessCoordinator +from .door import UnifiAccessDoor +from .hub import UnifiAccessHub + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Select entity for passed config entry.""" + hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] + + coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) + + await coordinator.async_config_entry_first_refresh() + + async_add_entities( + [ + TemporaryLockRuleSelectEntity(coordinator, door_id) + for door_id in coordinator.data + ] + ) + + +class TemporaryLockRuleSelectEntity(CoordinatorEntity, SelectEntity): + """Unifi Access Temporary Lock Rule Select.""" + + def __init__( + self, + coordinator: UnifiAccessCoordinator, + door_id: str, + ) -> None: + """Initialize Unifi Access Door Lock Rule.""" + super().__init__(coordinator, context="lock_rule") + self.door: UnifiAccessDoor = self.coordinator.data[door_id] + self._attr_unique_id = f"door_lock_rule_{door_id}" + self._attr_name = f"{self.door.name} Door Lock Rule" + self._attr_options = [ + "", + "keep_lock", + "keep_unlock", + "custom", + "reset", + "lock_early", + ] + self._update_options() + + @property + def device_info(self) -> DeviceInfo: + """Get Unifi Access Door Lock device information.""" + return DeviceInfo( + identifiers={(DOMAIN, self.door.id)}, + name=self.door.name, + model="UAH", + manufacturer="Unifi", + ) + + @property + def current_option(self) -> str: + "Get current option." + return self.door.lock_rule + + def _update_options(self): + "Update Door Lock Rules." + self._attr_current_option = self.coordinator.data[self.door.id].lock_rule + _LOGGER.debug( + "SelectEntity Update Lock Rule: %s, Current Options %s", + self.coordinator.data[self.door.id].lock_rule, + self._attr_options, + ) + if ( + self._attr_current_option != "schedule" + and "lock_early" in self._attr_options + ): + self._attr_options.remove("lock_early") + else: + self._attr_options.add("lock_early") + + async def async_select_option(self, option: str) -> None: + "Select Door Lock Rule." + await self.hass.async_add_executor_job(self.door.set_lock_rule, option) + + async def async_added_to_hass(self) -> None: + """Add Unifi Access Door Rule Lock Select to Home Assistant.""" + await super().async_added_to_hass() + self.door.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Remove Unifi Access Rule Lock Select from Home Assistant.""" + await super().async_will_remove_from_hass() + self.door.remove_callback(self.async_write_ha_state) + + def _handle_coordinator_update(self) -> None: + """Handle Unifi Access Door Lock updates from coordinator.""" + self._update_options() + self.async_write_ha_state() diff --git a/custom_components/unifi_access/sensor.py b/custom_components/unifi_access/sensor.py new file mode 100644 index 0000000..994e9a8 --- /dev/null +++ b/custom_components/unifi_access/sensor.py @@ -0,0 +1,70 @@ +"""Platform for number (interval) integration.""" + +import logging + +from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import UnifiAccessCoordinator +from .door import UnifiAccessDoor +from .hub import UnifiAccessHub + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Select entity for passed config entry.""" + hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] + + coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) + + await coordinator.async_config_entry_first_refresh() + + async_add_entities( + [TemporaryLockRuleSensorEntity(door) for door in coordinator.data.values()] + ) + + +class TemporaryLockRuleSensorEntity(SensorEntity): + """Unifi Access Temporary Lock Rule Sensor.""" + + def __init__(self, door: UnifiAccessDoor) -> None: + """Initialize Unifi Access Door Lock Rule Sensor.""" + super().__init__() + self.door: UnifiAccessDoor = door + self._attr_unique_id = f"door_lock_rule_sensor_{self.door.id}" + self._attr_name = f"{self.door.name} Current Door Lock Rule" + self._attr_native_value = f"{self.door.lock_rule}" + + @property + def device_info(self) -> DeviceInfo: + """Get Unifi Access Device information.""" + return DeviceInfo( + identifiers={(DOMAIN, self.door.id)}, + name=self.door.name, + model="UAH", + manufacturer="Unifi", + ) + + @property + def native_value(self) -> str: + """Get native value.""" + return self.door.lock_rule + + async def async_added_to_hass(self) -> None: + """Add Unifi Access Door Lock to Home Assistant.""" + await super().async_added_to_hass() + self.door.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Remove Unifi Access Door Lock from Home Assistant.""" + await super().async_will_remove_from_hass() + self.door.remove_callback(self.async_write_ha_state) diff --git a/custom_components/unifi_access/switch.py b/custom_components/unifi_access/switch.py new file mode 100644 index 0000000..eb0dad7 --- /dev/null +++ b/custom_components/unifi_access/switch.py @@ -0,0 +1,127 @@ +"""Platform for switch integration.""" + +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import UnifiAccessEvacuationAndLockdownSwitchCoordinator +from .hub import UnifiAccessHub + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Add Binary Sensor for passed config entry.""" + hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] + + coordinator: UnifiAccessEvacuationAndLockdownSwitchCoordinator = ( + UnifiAccessEvacuationAndLockdownSwitchCoordinator(hass, hub) + ) + + await coordinator.async_config_entry_first_refresh() + + async_add_entities( + [ + EvacuationSwitch(hass, hub, coordinator), + LockdownSwitch(hass, hub, coordinator), + ] + ) + + +class EvacuationSwitch(CoordinatorEntity, SwitchEntity): + """Unifi Access Evacuation Switch.""" + + def __init__( + self, + hass: HomeAssistant, + hub: UnifiAccessHub, + coordinator: UnifiAccessEvacuationAndLockdownSwitchCoordinator, + ) -> None: + """Initialize Unifi Access Evacuation Switch.""" + super().__init__(coordinator, context="evacuation") + self.hass = hass + self.hub = hub + self._is_on = self.hub.evacuation + self._attr_unique_id = "unifi_access_all_doors_evacuation" + self._attr_name = "Evacuation" + + @property + def device_info(self) -> DeviceInfo: + """Get Unifi Access Evacuation Switch device information.""" + return DeviceInfo( + identifiers={(DOMAIN, "unifi_access_all_doors")}, + name="All Doors", + model="UAH", + manufacturer="Unifi", + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + "Turn off Evacuation." + await self.hass.async_add_executor_job( + self.hub.set_doors_emergency_status, {"evacuation": False} + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + "Turn off Evacuation." + await self.hass.async_add_executor_job( + self.hub.set_doors_emergency_status, {"evacuation": True} + ) + + def _handle_coordinator_update(self) -> None: + """Handle Unifi Access Door Lock updates from coordinator.""" + self._attr_is_on = self.hub.evacuation + self.async_write_ha_state() + + +class LockdownSwitch(CoordinatorEntity, SwitchEntity): + """Unifi Access Lockdown Switch.""" + + def __init__( + self, + hass: HomeAssistant, + hub: UnifiAccessHub, + coordinator: UnifiAccessEvacuationAndLockdownSwitchCoordinator, + ) -> None: + """Initialize Unifi Access Lockdown Switch.""" + super().__init__(coordinator, context="lockdown") + self.hass = hass + self.hub = hub + self.coordinator = coordinator + self._attr_unique_id = "unifi_access_all_doors_lockdown" + self._is_on = self.hub.lockdown + self._attr_name = "Lockdown" + + @property + def device_info(self) -> DeviceInfo: + """Get Unifi Access Lockdown Switch device information.""" + return DeviceInfo( + identifiers={(DOMAIN, "unifi_access_all_doors")}, + name="All Doors", + model="UAH", + manufacturer="Unifi", + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + "Turn off Evacuation." + await self.hass.async_add_executor_job( + self.hub.set_doors_emergency_status, {"lockdown": False} + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + "Turn off Evacuation." + await self.hass.async_add_executor_job( + self.hub.set_doors_emergency_status, {"lockdown": True} + ) + + def _handle_coordinator_update(self) -> None: + """Handle Unifi Access Door Lock updates from coordinator.""" + self._attr_is_on = self.hub.lockdown + self.async_write_ha_state() From 09ab3c4a8b1d7c2da573f4f756f6257ccda124c2 Mon Sep 17 00:00:00 2001 From: imhotep Date: Sun, 11 Aug 2024 22:26:04 -0700 Subject: [PATCH 04/16] checking for door lock rule support --- custom_components/unifi_access/hub.py | 26 +++++--- custom_components/unifi_access/number.py | 19 +++--- custom_components/unifi_access/select.py | 19 +++--- custom_components/unifi_access/sensor.py | 78 +++++++++++++++++++++--- 4 files changed, 109 insertions(+), 33 deletions(-) diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 73d82fa..726e648 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -94,6 +94,7 @@ def __init__( self._doors: dict[str, UnifiAccessDoor] = {} self.evacuation = False self.lockdown = False + self.supports_door_lock_rules = True self.update_t = None self.loop = asyncio.get_event_loop() @@ -110,16 +111,19 @@ def set_api_token(self, api_token): def update(self): """Get latest door data.""" - _LOGGER.info( - "Getting door updates from Unifi Access %s Use Polling %s", + _LOGGER.debug( + "Getting door updates from Unifi Access %s Use Polling %s. Doors? %s", self.host, self.use_polling, + self.doors, ) data = self._make_http_request(f"{self.host}{DOORS_URL}") for _i, door in enumerate(data): door_id = door["id"] - door_lock_rule = self.get_door_lock_rule(door_id) + door_lock_rule = {"type": "", "ended_time": 0} + if self.supports_door_lock_rules: + door_lock_rule = self.get_door_lock_rule(door_id) if door_id in self.doors: existing_door = self.doors[door_id] existing_door.name = door["name"] @@ -182,14 +186,18 @@ def authenticate(self, api_token: str) -> str: return "ok" - def get_door_lock_rule(self, door_id: str) -> DoorLockRule: + def get_door_lock_rule(self, door_id: str) -> DoorLockRule | None: """Get door lock rule.""" _LOGGER.debug("Getting door lock rule for door_id %s", door_id) - data = self._make_http_request( - f"{self.host}{DOOR_LOCK_RULE_URL}".format(door_id=door_id) - ) - _LOGGER.debug("Got door lock rule for door_id %s %s", door_id, data) - return cast(DoorLockRule, data) + try: + data = self._make_http_request( + f"{self.host}{DOOR_LOCK_RULE_URL}".format(door_id=door_id) + ) + _LOGGER.debug("Got door lock rule for door_id %s %s", door_id, data) + return cast(DoorLockRule, data) + except ApiError: + self.supports_door_lock_rules = False + _LOGGER.debug("cannot get door lock rule. Likely unsupported hub") def set_door_lock_rule(self, door_id: str, door_lock_rule: DoorLockRule) -> None: """Set door lock rule.""" diff --git a/custom_components/unifi_access/number.py b/custom_components/unifi_access/number.py index c9cd62a..b5f9c4f 100644 --- a/custom_components/unifi_access/number.py +++ b/custom_components/unifi_access/number.py @@ -27,24 +27,27 @@ async def async_setup_entry( coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) await coordinator.async_config_entry_first_refresh() - - async_add_entities( - [ - TemporaryLockRuleIntervalNumberEntity(door) - for door in coordinator.data.values() - ] - ) + if hub.supports_door_lock_rules: + async_add_entities( + [ + TemporaryLockRuleIntervalNumberEntity(door) + for door in coordinator.data.values() + ] + ) class TemporaryLockRuleIntervalNumberEntity(RestoreNumber): """Unifi Access Temporary Lock Rule Interval Interval.""" + _attr_translation_key = "door_lock_rule_interval" + _attr_has_entity_name = True + should_poll = False + def __init__(self, door: UnifiAccessDoor) -> None: """Initialize Unifi Access Door Lock Rule Interval.""" super().__init__() self.door: UnifiAccessDoor = door self._attr_unique_id = f"door_lock_rule_interval_{self.door.id}" - self._attr_name = f"{self.door.name} Door Lock Rule Interval (min)" self._attr_native_value = 10 self._attr_native_min_value = 1 self._attr_native_max_value = 480 diff --git a/custom_components/unifi_access/select.py b/custom_components/unifi_access/select.py index 9847ebc..3a2988b 100644 --- a/custom_components/unifi_access/select.py +++ b/custom_components/unifi_access/select.py @@ -24,18 +24,22 @@ async def async_setup_entry( coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) await coordinator.async_config_entry_first_refresh() - - async_add_entities( - [ - TemporaryLockRuleSelectEntity(coordinator, door_id) - for door_id in coordinator.data - ] - ) + if hub.supports_door_lock_rules: + async_add_entities( + [ + TemporaryLockRuleSelectEntity(coordinator, door_id) + for door_id in coordinator.data + ] + ) class TemporaryLockRuleSelectEntity(CoordinatorEntity, SelectEntity): """Unifi Access Temporary Lock Rule Select.""" + _attr_translation_key = "door_lock_rules" + _attr_has_entity_name = True + should_poll = False + def __init__( self, coordinator: UnifiAccessCoordinator, @@ -45,7 +49,6 @@ def __init__( super().__init__(coordinator, context="lock_rule") self.door: UnifiAccessDoor = self.coordinator.data[door_id] self._attr_unique_id = f"door_lock_rule_{door_id}" - self._attr_name = f"{self.door.name} Door Lock Rule" self._attr_options = [ "", "keep_lock", diff --git a/custom_components/unifi_access/sensor.py b/custom_components/unifi_access/sensor.py index d887dc6..7d5d7c6 100644 --- a/custom_components/unifi_access/sensor.py +++ b/custom_components/unifi_access/sensor.py @@ -1,6 +1,6 @@ """Platform for number (interval) integration.""" -import logging +from datetime import UTC, datetime from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry @@ -13,8 +13,6 @@ from .door import UnifiAccessDoor from .hub import UnifiAccessHub -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry( hass: HomeAssistant, @@ -28,20 +26,36 @@ async def async_setup_entry( await coordinator.async_config_entry_first_refresh() - async_add_entities( - [TemporaryLockRuleSensorEntity(door) for door in coordinator.data.values()] - ) + if hub.supports_door_lock_rules: + async_add_entities( + [ + sensor_entity(door) + for door in coordinator.data.values() + for sensor_entity in ( + TemporaryLockRuleSensorEntity, + TemporaryLockRuleEndTimeSensorEntity, + ) + ] + ) + + +async def async_remove_entry(hass, entry) -> None: + """Handle removal of an entry.""" class TemporaryLockRuleSensorEntity(SensorEntity): """Unifi Access Temporary Lock Rule Sensor.""" + should_poll = False + + _attr_translation_key = "door_lock_rule" + _attr_has_entity_name = True + def __init__(self, door: UnifiAccessDoor) -> None: """Initialize Unifi Access Door Lock Rule Sensor.""" super().__init__() self.door: UnifiAccessDoor = door self._attr_unique_id = f"door_lock_rule_sensor_{self.door.id}" - self._attr_name = f"{self.door.name} Current Door Lock Rule" self._attr_native_value = f"{self.door.lock_rule}" @property @@ -57,7 +71,6 @@ def device_info(self) -> DeviceInfo: @property def native_value(self) -> str: """Get native value.""" - # TODO add end time to native value with converted timezone from self.door.lock_rule_ended_time and format as string # pylint: disable=fixme return self.door.lock_rule async def async_added_to_hass(self) -> None: @@ -69,3 +82,52 @@ async def async_will_remove_from_hass(self) -> None: """Remove Unifi Access Door Lock from Home Assistant.""" await super().async_will_remove_from_hass() self.door.remove_callback(self.async_write_ha_state) + + +class TemporaryLockRuleEndTimeSensorEntity(SensorEntity): + """Unifi Access Temporary Lock Rule Sensor End Time.""" + + should_poll = False + + _attr_translation_key = "door_lock_rule_ended_time" + _attr_has_entity_name = True + + def __init__(self, door: UnifiAccessDoor) -> None: + """Initialize Unifi Access Door Lock Rule Sensor End Time.""" + super().__init__() + self.door: UnifiAccessDoor = door + self._attr_unique_id = f"door_lock_rule_sensor_ended_time_{self.door.id}" + self._attr_native_value = self._get_ended_time() + + @property + def device_info(self) -> DeviceInfo: + """Get Unifi Access Device information.""" + return DeviceInfo( + identifiers={(DOMAIN, self.door.id)}, + name=self.door.name, + model=self.door.hub_type, + manufacturer="Unifi", + ) + + def _get_ended_time(self): + if self.door.lock_rule_ended_time and int(self.door.lock_rule_ended_time) != 0: + utc_timestamp = int(self.door.lock_rule_ended_time) + utc_datetime = datetime.fromtimestamp(utc_timestamp, tz=UTC) + local_datetime = utc_datetime.astimezone() + return f" {local_datetime.strftime("%Y-%m-%d %H:%M:%S %Z")}" + return "" + + @property + def native_value(self) -> str: + """Get native value.""" + return self._get_ended_time() + + async def async_added_to_hass(self) -> None: + """Add Unifi Access Door Lock to Home Assistant.""" + await super().async_added_to_hass() + self.door.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Remove Unifi Access Door Lock from Home Assistant.""" + await super().async_will_remove_from_hass() + self.door.remove_callback(self.async_write_ha_state) From 0765aa400669edf1074bd526fdb6a52e2e2c733c Mon Sep 17 00:00:00 2001 From: imhotep Date: Sun, 11 Aug 2024 22:29:55 -0700 Subject: [PATCH 05/16] transalation keys --- custom_components/unifi_access/__init__.py | 1 - custom_components/unifi_access/binary_sensor.py | 3 ++- custom_components/unifi_access/coordinator.py | 1 + custom_components/unifi_access/event.py | 10 ++++++++-- custom_components/unifi_access/lock.py | 5 ++++- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/custom_components/unifi_access/__init__.py b/custom_components/unifi_access/__init__.py index c90e976..d077285 100644 --- a/custom_components/unifi_access/__init__.py +++ b/custom_components/unifi_access/__init__.py @@ -27,7 +27,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hub.set_api_token(entry.data["api_token"]) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = hub - await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/custom_components/unifi_access/binary_sensor.py b/custom_components/unifi_access/binary_sensor.py index 68fbc67..1a3a5b0 100644 --- a/custom_components/unifi_access/binary_sensor.py +++ b/custom_components/unifi_access/binary_sensor.py @@ -44,6 +44,8 @@ class UnifiDoorStatusEntity(CoordinatorEntity, BinarySensorEntity): """Unifi Access DPS Entity.""" should_poll = False + _attr_translation_key = "access_door_dps" + _attr_has_entity_name = True def __init__(self, coordinator, door_id) -> None: """Initialize DPS Entity.""" @@ -52,7 +54,6 @@ def __init__(self, coordinator, door_id) -> None: self.door = self.coordinator.data[door_id] self._attr_unique_id = self.door.id self.device_name = self.door.name - self._attr_name = f"{self.door.name} Door Position Sensor" self._attr_available = self.door.door_position_status is not None self._attr_is_on = self.door.door_position_status == "open" diff --git a/custom_components/unifi_access/coordinator.py b/custom_components/unifi_access/coordinator.py index 9e9585f..399a915 100644 --- a/custom_components/unifi_access/coordinator.py +++ b/custom_components/unifi_access/coordinator.py @@ -27,6 +27,7 @@ def __init__(self, hass: HomeAssistant, hub) -> None: hass, _LOGGER, name="Unifi Access Coordinator", + always_update=False, update_interval=update_interval, ) self.hub = hub diff --git a/custom_components/unifi_access/event.py b/custom_components/unifi_access/event.py index b9e7e62..e431cd4 100644 --- a/custom_components/unifi_access/event.py +++ b/custom_components/unifi_access/event.py @@ -48,13 +48,16 @@ class AccessEventEntity(EventEntity): """Authorized User Event Entity.""" _attr_event_types = [ACCESS_ENTRY_EVENT, ACCESS_EXIT_EVENT] + _attr_translation_key = "access_event" + _attr_has_entity_name = True + should_poll = False def __init__(self, hass: HomeAssistant, door) -> None: """Initialize Unifi Access Door Lock.""" self.hass = hass self.door: UnifiAccessDoor = door self._attr_unique_id = f"{self.door.id}_access" - self._attr_name = f"{self.door.name} Access" + self._attr_translation_placeholders = {"door_name": self.door.name} @property def device_info(self) -> DeviceInfo: @@ -88,13 +91,16 @@ class DoorbellPressedEventEntity(EventEntity): _attr_device_class = EventDeviceClass.DOORBELL _attr_event_types = [DOORBELL_START_EVENT, DOORBELL_STOP_EVENT] + _attr_translation_key = "doorbell_event" + _attr_has_entity_name = True + should_poll = False def __init__(self, hass: HomeAssistant, door) -> None: """Initialize Unifi Access Doorbell Event.""" self.hass = hass self.door: UnifiAccessDoor = door self._attr_unique_id = f"{self.door.id}_doorbell_press" - self._attr_name = f"{self.door.name} Doorbell Press" + self._attr_translation_placeholders = {"door_name": self.door.name} @property def device_info(self) -> DeviceInfo: diff --git a/custom_components/unifi_access/lock.py b/custom_components/unifi_access/lock.py index 613400a..5d0eb10 100644 --- a/custom_components/unifi_access/lock.py +++ b/custom_components/unifi_access/lock.py @@ -44,12 +44,15 @@ class UnifiDoorLockEntity(CoordinatorEntity, LockEntity): supported_features = LockEntityFeature.OPEN + _attr_translation_key = "access_door" + _attr_has_entity_name = True + def __init__(self, coordinator, door_id) -> None: """Initialize Unifi Access Door Lock.""" super().__init__(coordinator, context=id) self.door: UnifiAccessDoor = self.coordinator.data[door_id] self._attr_unique_id = self.door.id - self._attr_name = self.door.name + self._attr_translation_placeholders = {"door_name": self.door.name} @property def device_info(self) -> DeviceInfo: From ef07dacbe70cbabb4c4a68b0bab14092d8948b00 Mon Sep 17 00:00:00 2001 From: imhotep Date: Sun, 11 Aug 2024 22:33:24 -0700 Subject: [PATCH 06/16] typos --- custom_components/unifi_access/hub.py | 1 + custom_components/unifi_access/sensor.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 726e648..fd87d89 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -198,6 +198,7 @@ def get_door_lock_rule(self, door_id: str) -> DoorLockRule | None: except ApiError: self.supports_door_lock_rules = False _LOGGER.debug("cannot get door lock rule. Likely unsupported hub") + return None def set_door_lock_rule(self, door_id: str, door_lock_rule: DoorLockRule) -> None: """Set door lock rule.""" diff --git a/custom_components/unifi_access/sensor.py b/custom_components/unifi_access/sensor.py index 7d5d7c6..bc90048 100644 --- a/custom_components/unifi_access/sensor.py +++ b/custom_components/unifi_access/sensor.py @@ -39,10 +39,6 @@ async def async_setup_entry( ) -async def async_remove_entry(hass, entry) -> None: - """Handle removal of an entry.""" - - class TemporaryLockRuleSensorEntity(SensorEntity): """Unifi Access Temporary Lock Rule Sensor.""" From 4a13e516cfded1738ac2487740f280f75fc3506b Mon Sep 17 00:00:00 2001 From: imhotep Date: Mon, 12 Aug 2024 18:20:41 -0700 Subject: [PATCH 07/16] adding KeyError to exception list --- custom_components/unifi_access/hub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index fd87d89..727184e 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -94,7 +94,7 @@ def __init__( self._doors: dict[str, UnifiAccessDoor] = {} self.evacuation = False self.lockdown = False - self.supports_door_lock_rules = True + self.supports_door_lock_rules = False self.update_t = None self.loop = asyncio.get_event_loop() @@ -195,7 +195,7 @@ def get_door_lock_rule(self, door_id: str) -> DoorLockRule | None: ) _LOGGER.debug("Got door lock rule for door_id %s %s", door_id, data) return cast(DoorLockRule, data) - except ApiError: + except (ApiError, KeyError): self.supports_door_lock_rules = False _LOGGER.debug("cannot get door lock rule. Likely unsupported hub") return None From d4de07e5b1ee1368d3a66205279624bee297ee04 Mon Sep 17 00:00:00 2001 From: imhotep Date: Mon, 12 Aug 2024 18:23:07 -0700 Subject: [PATCH 08/16] setting support door locking rule to true --- custom_components/unifi_access/hub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 727184e..0d45c3b 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -94,7 +94,7 @@ def __init__( self._doors: dict[str, UnifiAccessDoor] = {} self.evacuation = False self.lockdown = False - self.supports_door_lock_rules = False + self.supports_door_lock_rules = True self.update_t = None self.loop = asyncio.get_event_loop() From 539850bf119cc17442e9f42aeea629d6eea1256f Mon Sep 17 00:00:00 2001 From: imhotep Date: Mon, 12 Aug 2024 22:02:33 -0700 Subject: [PATCH 09/16] default door lock rule for unsupported hub --- custom_components/unifi_access/hub.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 0d45c3b..6ae44a2 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -51,7 +51,7 @@ class DoorLockRuleStatus(TypedDict): This class defines the active locking rule status. """ - type: Literal["schedule", "keep_lock", "keep_unlock", "custom", "lock_early"] + type: Literal["schedule", "keep_lock", "keep_unlock", "custom", "lock_early", ""] ended_time: int @@ -186,7 +186,7 @@ def authenticate(self, api_token: str) -> str: return "ok" - def get_door_lock_rule(self, door_id: str) -> DoorLockRule | None: + def get_door_lock_rule(self, door_id: str) -> DoorLockRuleStatus: """Get door lock rule.""" _LOGGER.debug("Getting door lock rule for door_id %s", door_id) try: @@ -194,11 +194,11 @@ def get_door_lock_rule(self, door_id: str) -> DoorLockRule | None: f"{self.host}{DOOR_LOCK_RULE_URL}".format(door_id=door_id) ) _LOGGER.debug("Got door lock rule for door_id %s %s", door_id, data) - return cast(DoorLockRule, data) + return cast(DoorLockRuleStatus, data) except (ApiError, KeyError): self.supports_door_lock_rules = False _LOGGER.debug("cannot get door lock rule. Likely unsupported hub") - return None + return {"type": "", "ended_time": 0} def set_door_lock_rule(self, door_id: str, door_lock_rule: DoorLockRule) -> None: """Set door lock rule.""" From ca65c28cc25f3a8607bd7f80cdc0d1d8f8ab08a8 Mon Sep 17 00:00:00 2001 From: imhotep Date: Mon, 12 Aug 2024 22:25:04 -0700 Subject: [PATCH 10/16] updating documentation --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 61199ee..1c94572 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,15 @@ # Unifi Access Custom Integration for Home Assistant -This is a basic integration of [Unifi Access](https://ui.com/door-access) in [Home Assistant](https://homeassistant.io). If you have Unifi Access set up with UID this will *NOT* work. _Camera Feeds are currently not offered by the API and therefore **NOT** supported_. +This is a basic integration of [Unifi Access](https://ui.com/door-access) in [Home Assistant](https://homeassistant.io). If you have Unifi Access set up with UID this will likely *NOT* work although some people have reported success using the free version of UID. _Camera Feeds are currently not offered by the API and therefore **NOT** supported_. + +# Supported hardware +Unifi Access Hub (UAH) :white_check_mark: +Unifi Access Hub Enterprise (UAH-Ent) :x: (partial/experimental support) +Unifi Gate Hub (UGT) :x: (partial/experimental support) # Getting Unifi Access API Token - Log in to Unifi Access and Click on Security -> Advanced -> API Token -- Create a new token and pick all permissions (this is *IMPORTANT*) +- Create a new token and pick all permissions (this is *IMPORTANT*). At the very least pick: Space, Device and System Logs. # Installation (HACS) - Add this repository as a custom repository in HACS and install the integration. @@ -56,6 +61,18 @@ An entity will get created for each door. Every time a door is accessed (entry, - actor # this is the name of the user that accessed the door. If set to N/A that means UNAUTHORIZED ACCESS! - type # `unifi_access_entry` or `unifi_access_exit` +### Evacuation/Lockdown +The evacuation (unlock all doors) and lockdown (lock all doors) switches apply to all doors and gates and **will sound the alarm** no matter which configuration you currently have in your terminal settings. The status will not update currently (known issue). + +### Door lock rules (only applies to UAH) +The following entities will be created: input_select, input_number and 2 sensors (end time and current rule). +You are able to select one of the following rules via the input_select: +- keep_lock : door is locked indefinitely +- keep_unlock: door is unlocked indefinitely +- custom: door is unlocked for a given interval (use the input_number to define how long. Default is 10 minutes). +- reset: clear all lock rules +- lock_early: locks the door if it's currently on an unlock schedule. + # Example automation ``` From ec0091147adc525fe5cfde0f9920f4c2ad6d4c62 Mon Sep 17 00:00:00 2001 From: Anis Kadri Date: Mon, 12 Aug 2024 22:31:13 -0700 Subject: [PATCH 11/16] Update README.md --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1c94572..925a777 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ # Unifi Access Custom Integration for Home Assistant -This is a basic integration of [Unifi Access](https://ui.com/door-access) in [Home Assistant](https://homeassistant.io). If you have Unifi Access set up with UID this will likely *NOT* work although some people have reported success using the free version of UID. _Camera Feeds are currently not offered by the API and therefore **NOT** supported_. +- This is a basic integration of [Unifi Access](https://ui.com/door-access) in [Home Assistant](https://homeassistant.io). +- If you have Unifi Access set up with UID this will likely *NOT* work although some people have reported success using the free version of UID. +- _Camera Feeds are currently not offered by the API and therefore **NOT** supported_. # Supported hardware -Unifi Access Hub (UAH) :white_check_mark: -Unifi Access Hub Enterprise (UAH-Ent) :x: (partial/experimental support) -Unifi Gate Hub (UGT) :x: (partial/experimental support) +- Unifi Access Hub (UAH) :white_check_mark: +- Unifi Access Hub Enterprise (UAH-Ent) :x: (partial/experimental support) +- Unifi Gate Hub (UGT) :x: (partial/experimental support) # Getting Unifi Access API Token - Log in to Unifi Access and Click on Security -> Advanced -> API Token -- Create a new token and pick all permissions (this is *IMPORTANT*). At the very least pick: Space, Device and System Logs. +- Create a new token and pick all permissions (this is *IMPORTANT*). At the very least pick: Space, Device and System Log. # Installation (HACS) - Add this repository as a custom repository in HACS and install the integration. @@ -65,13 +67,13 @@ An entity will get created for each door. Every time a door is accessed (entry, The evacuation (unlock all doors) and lockdown (lock all doors) switches apply to all doors and gates and **will sound the alarm** no matter which configuration you currently have in your terminal settings. The status will not update currently (known issue). ### Door lock rules (only applies to UAH) -The following entities will be created: input_select, input_number and 2 sensors (end time and current rule). -You are able to select one of the following rules via the input_select: -- keep_lock : door is locked indefinitely -- keep_unlock: door is unlocked indefinitely -- custom: door is unlocked for a given interval (use the input_number to define how long. Default is 10 minutes). -- reset: clear all lock rules -- lock_early: locks the door if it's currently on an unlock schedule. +The following entities will be created: `input_select`, `input_number` and 2 `sensor` entities (end time and current rule). +You are able to select one of the following rules via the `input_select`: +- **keep_lock**: door is locked indefinitely +- **keep_unlock**: door is unlocked indefinitely +- **custom**: door is unlocked for a given interval (use the input_number to define how long. Default is 10 minutes). +- **reset**: clear all lock rules +- **lock_early**: locks the door if it's currently on an unlock schedule. # Example automation From 0723731d884c78500e0b5815b41a3601bcbd6971 Mon Sep 17 00:00:00 2001 From: iviemeister Date: Sun, 18 Aug 2024 19:38:39 -0400 Subject: [PATCH 12/16] Initial UA-ULTRA support --- README.md | 1 + custom_components/unifi_access/door.py | 3 +++ custom_components/unifi_access/hub.py | 2 ++ 3 files changed, 6 insertions(+) diff --git a/README.md b/README.md index 925a777..dac4e13 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - Unifi Access Hub (UAH) :white_check_mark: - Unifi Access Hub Enterprise (UAH-Ent) :x: (partial/experimental support) - Unifi Gate Hub (UGT) :x: (partial/experimental support) +- Unifi Access Ultra :x: (partial/experimental support) # Getting Unifi Access API Token - Log in to Unifi Access and Click on Security -> Advanced -> API Token diff --git a/custom_components/unifi_access/door.py b/custom_components/unifi_access/door.py index 14f5a49..114d9df 100644 --- a/custom_components/unifi_access/door.py +++ b/custom_components/unifi_access/door.py @@ -56,6 +56,9 @@ def is_open(self): @property def is_locked(self): """Solely used for locked state when calling lock.""" + if self.door_lock_relay_status == "" : + self.door_lock_relay_status = "lock" + _LOGGER.warning("door.py: self.door_lock_relay_status not set - assuming locked") return self.door_lock_relay_status == "lock" @property diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index 6ae44a2..c1a4414 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -431,6 +431,8 @@ def _handle_config_update(self, update, device_type): return self._handle_UAH_config_update(update, device_type) case "UAH-Ent": return self._handle_UAH_Ent_config_update(update, device_type) + case "UA-ULTRA": + return self._handle_UAH_Ent_config_update(update, device_type) case "UGT": return self._handle_UGT_config_update(update, device_type) From d8b2e4580937f6e56d7715f564e29c5016746e92 Mon Sep 17 00:00:00 2001 From: imhotep Date: Sun, 25 Aug 2024 13:22:33 -0700 Subject: [PATCH 13/16] improve coordinator performance and fix evacuation/lockdown --- custom_components/unifi_access/__init__.py | 8 ++++ .../unifi_access/binary_sensor.py | 4 +- custom_components/unifi_access/door.py | 6 +-- custom_components/unifi_access/event.py | 7 +-- custom_components/unifi_access/hub.py | 28 ++++++++++- custom_components/unifi_access/lock.py | 7 +-- custom_components/unifi_access/manifest.json | 2 +- custom_components/unifi_access/number.py | 4 +- custom_components/unifi_access/select.py | 3 +- custom_components/unifi_access/sensor.py | 5 +- custom_components/unifi_access/switch.py | 47 +++++++++++++++++-- 11 files changed, 88 insertions(+), 33 deletions(-) diff --git a/custom_components/unifi_access/__init__.py b/custom_components/unifi_access/__init__.py index d077285..d4372ab 100644 --- a/custom_components/unifi_access/__init__.py +++ b/custom_components/unifi_access/__init__.py @@ -7,6 +7,7 @@ from homeassistant.core import HomeAssistant from .const import DOMAIN +from .coordinator import UnifiAccessCoordinator from .hub import UnifiAccessHub PLATFORMS: list[Platform] = [ @@ -27,6 +28,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hub.set_api_token(entry.data["api_token"]) hass.data.setdefault(DOMAIN, {})[entry.entry_id] = hub + + coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) + + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN]["coordinator"] = coordinator + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/custom_components/unifi_access/binary_sensor.py b/custom_components/unifi_access/binary_sensor.py index 1a3a5b0..a580f6e 100644 --- a/custom_components/unifi_access/binary_sensor.py +++ b/custom_components/unifi_access/binary_sensor.py @@ -15,7 +15,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import UnifiAccessCoordinator from .hub import UnifiAccessHub _LOGGER = logging.getLogger(__name__) @@ -29,9 +28,8 @@ async def async_setup_entry( """Add Binary Sensor for passed config entry.""" hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] - coordinator = UnifiAccessCoordinator(hass, hub) + coordinator = hass.data[DOMAIN]["coordinator"] - await coordinator.async_config_entry_first_refresh() binary_sensor_entities: list[UnifiDoorStatusEntity | UnifiDoorbellStatusEntity] = [] for key in coordinator.data: binary_sensor_entities.append(UnifiDoorStatusEntity(coordinator, key)) diff --git a/custom_components/unifi_access/door.py b/custom_components/unifi_access/door.py index 114d9df..54b64c2 100644 --- a/custom_components/unifi_access/door.py +++ b/custom_components/unifi_access/door.py @@ -56,9 +56,9 @@ def is_open(self): @property def is_locked(self): """Solely used for locked state when calling lock.""" - if self.door_lock_relay_status == "" : + if self.door_lock_relay_status == "": self.door_lock_relay_status = "lock" - _LOGGER.warning("door.py: self.door_lock_relay_status not set - assuming locked") + _LOGGER.warning("Relay status not set - assuming locked") return self.door_lock_relay_status == "lock" @property @@ -97,7 +97,7 @@ def get_lock_rule(self) -> None: self._hub.get_door_lock_rule(self._id) def register_callback(self, callback: Callable[[], None]) -> None: - """Register callback, called when Roller changes state.""" + """Register callback, called when door changes state.""" self._callbacks.add(callback) def remove_callback(self, callback: Callable[[], None]) -> None: diff --git a/custom_components/unifi_access/event.py b/custom_components/unifi_access/event.py index e431cd4..fab4522 100644 --- a/custom_components/unifi_access/event.py +++ b/custom_components/unifi_access/event.py @@ -17,9 +17,7 @@ DOORBELL_START_EVENT, DOORBELL_STOP_EVENT, ) -from .coordinator import UnifiAccessCoordinator from .door import UnifiAccessDoor -from .hub import UnifiAccessHub _LOGGER = logging.getLogger(__name__) @@ -30,11 +28,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add Binary Sensor for passed config entry.""" - hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] - coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) - - await coordinator.async_config_entry_first_refresh() + coordinator = hass.data[DOMAIN]["coordinator"] async_add_entities( (AccessEventEntity(hass, door) for door in coordinator.data.values()), diff --git a/custom_components/unifi_access/hub.py b/custom_components/unifi_access/hub.py index c1a4414..acd8ed7 100644 --- a/custom_components/unifi_access/hub.py +++ b/custom_components/unifi_access/hub.py @@ -4,6 +4,7 @@ """ import asyncio +from collections.abc import Callable import json import logging import ssl @@ -96,6 +97,7 @@ def __init__( self.lockdown = False self.supports_door_lock_rules = True self.update_t = None + self._callbacks: set[Callable] = set() self.loop = asyncio.get_event_loop() @property @@ -229,6 +231,9 @@ def set_doors_emergency_status(self, emergency_data: EmergencyData) -> None: self._make_http_request( f"{self.host}{DOORS_EMERGENCY_URL}", "PUT", emergency_data ) + self.evacuation = emergency_data.get("evacuation", self.evacuation) + self.lockdown = emergency_data.get("lockdown", self.lockdown) + _LOGGER.debug("Emergency status set %s", emergency_data) def unlock_door(self, door_id: str) -> None: """Test if we can authenticate with the host.""" @@ -421,7 +426,6 @@ def _handle_UGT_config_update(self, update, device_type): == "on" else "lock" ) - # TODO find config keys for temporary lock rules and their ended time return existing_door def _handle_config_update(self, update, device_type): @@ -625,6 +629,15 @@ def on_complete(_fut): update["data"]["request_id"], ) changed_doors.append(existing_door) + case "access.data.setting.update": + self.evacuation = update["data"]["evacuation"] + self.lockdown = update["data"]["lockdown"] + asyncio.run_coroutine_threadsafe(self.publish_updates(), self.loop) + _LOGGER.info( + "Settings updated. Evacuation %s, Lockdown %s", + self.evacuation, + self.lockdown, + ) case _: _LOGGER.debug("unhandled websocket message %s", update["event"]) @@ -687,3 +700,16 @@ def listen_for_updates(self): if self.verify_ssl is False: sslopt = {"cert_reqs": ssl.CERT_NONE} ws.run_forever(sslopt=sslopt, reconnect=5) + + def register_callback(self, callback: Callable[[], None]) -> None: + """Register callback, called when settings change.""" + self._callbacks.add(callback) + + def remove_callback(self, callback: Callable[[], None]) -> None: + """Remove previously registered callback.""" + self._callbacks.discard(callback) + + async def publish_updates(self) -> None: + """Schedule call all registered callbacks.""" + for callback in self._callbacks: + callback() diff --git a/custom_components/unifi_access/lock.py b/custom_components/unifi_access/lock.py index 5d0eb10..f6d2ca7 100644 --- a/custom_components/unifi_access/lock.py +++ b/custom_components/unifi_access/lock.py @@ -13,9 +13,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DOMAIN -from .coordinator import UnifiAccessCoordinator from .door import UnifiAccessDoor -from .hub import UnifiAccessHub _LOGGER = logging.getLogger(__name__) @@ -26,11 +24,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Add Binary Sensor for passed config entry.""" - hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] - coordinator = UnifiAccessCoordinator(hass, hub) - - await coordinator.async_config_entry_first_refresh() + coordinator = hass.data[DOMAIN]["coordinator"] async_add_entities( UnifiDoorLockEntity(coordinator, key) for key in coordinator.data diff --git a/custom_components/unifi_access/manifest.json b/custom_components/unifi_access/manifest.json index a1d570d..4e79329 100644 --- a/custom_components/unifi_access/manifest.json +++ b/custom_components/unifi_access/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/unifi_access", "issue_tracker": "https://github.com/imhotep/hass-unifi-access/issues", "iot_class": "local_push", - "version": "1.1.6", + "version": "1.2.0", "requirements": ["websocket-client==1.8.0"] } \ No newline at end of file diff --git a/custom_components/unifi_access/number.py b/custom_components/unifi_access/number.py index b5f9c4f..e3ecbbc 100644 --- a/custom_components/unifi_access/number.py +++ b/custom_components/unifi_access/number.py @@ -9,7 +9,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import UnifiAccessCoordinator from .door import UnifiAccessDoor from .hub import UnifiAccessHub @@ -24,9 +23,8 @@ async def async_setup_entry( """Add Select entity for passed config entry.""" hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] - coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) + coordinator = hass.data[DOMAIN]["coordinator"] - await coordinator.async_config_entry_first_refresh() if hub.supports_door_lock_rules: async_add_entities( [ diff --git a/custom_components/unifi_access/select.py b/custom_components/unifi_access/select.py index 3a2988b..be6a657 100644 --- a/custom_components/unifi_access/select.py +++ b/custom_components/unifi_access/select.py @@ -21,9 +21,8 @@ async def async_setup_entry( """Add Select entity for passed config entry.""" hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] - coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) + coordinator = hass.data[DOMAIN]["coordinator"] - await coordinator.async_config_entry_first_refresh() if hub.supports_door_lock_rules: async_add_entities( [ diff --git a/custom_components/unifi_access/sensor.py b/custom_components/unifi_access/sensor.py index bc90048..077c1b9 100644 --- a/custom_components/unifi_access/sensor.py +++ b/custom_components/unifi_access/sensor.py @@ -9,7 +9,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN -from .coordinator import UnifiAccessCoordinator from .door import UnifiAccessDoor from .hub import UnifiAccessHub @@ -22,9 +21,7 @@ async def async_setup_entry( """Add Select entity for passed config entry.""" hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id] - coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub) - - await coordinator.async_config_entry_first_refresh() + coordinator = hass.data[DOMAIN]["coordinator"] if hub.supports_door_lock_rules: async_add_entities( diff --git a/custom_components/unifi_access/switch.py b/custom_components/unifi_access/switch.py index eb0dad7..f3be23c 100644 --- a/custom_components/unifi_access/switch.py +++ b/custom_components/unifi_access/switch.py @@ -1,5 +1,6 @@ """Platform for switch integration.""" +import logging from typing import Any from homeassistant.components.switch import SwitchEntity @@ -13,6 +14,8 @@ from .coordinator import UnifiAccessEvacuationAndLockdownSwitchCoordinator from .hub import UnifiAccessHub +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry( hass: HomeAssistant, @@ -39,6 +42,10 @@ async def async_setup_entry( class EvacuationSwitch(CoordinatorEntity, SwitchEntity): """Unifi Access Evacuation Switch.""" + _attr_translation_key = "evacuation" + _attr_has_entity_name = True + should_poll = False + def __init__( self, hass: HomeAssistant, @@ -51,7 +58,6 @@ def __init__( self.hub = hub self._is_on = self.hub.evacuation self._attr_unique_id = "unifi_access_all_doors_evacuation" - self._attr_name = "Evacuation" @property def device_info(self) -> DeviceInfo: @@ -63,6 +69,11 @@ def device_info(self) -> DeviceInfo: manufacturer="Unifi", ) + @property + def is_on(self) -> bool: + """Get Unifi Access Evacuation Switch status.""" + return self.hub.evacuation + async def async_turn_off(self, **kwargs: Any) -> None: "Turn off Evacuation." await self.hass.async_add_executor_job( @@ -76,14 +87,28 @@ async def async_turn_on(self, **kwargs: Any) -> None: ) def _handle_coordinator_update(self) -> None: - """Handle Unifi Access Door Lock updates from coordinator.""" + """Handle Unifi Access Evacuation Switch updates from coordinator.""" self._attr_is_on = self.hub.evacuation self.async_write_ha_state() + async def async_added_to_hass(self) -> None: + """Add Unifi Access Evacuation Switch to Home Assistant.""" + await super().async_added_to_hass() + self.hub.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Remove Unifi Access Evacuation Switch from Home Assistant.""" + await super().async_will_remove_from_hass() + self.hub.remove_callback(self.async_write_ha_state) + class LockdownSwitch(CoordinatorEntity, SwitchEntity): """Unifi Access Lockdown Switch.""" + _attr_translation_key = "lockdown" + _attr_has_entity_name = True + should_poll = False + def __init__( self, hass: HomeAssistant, @@ -97,7 +122,6 @@ def __init__( self.coordinator = coordinator self._attr_unique_id = "unifi_access_all_doors_lockdown" self._is_on = self.hub.lockdown - self._attr_name = "Lockdown" @property def device_info(self) -> DeviceInfo: @@ -109,6 +133,11 @@ def device_info(self) -> DeviceInfo: manufacturer="Unifi", ) + @property + def is_on(self) -> bool: + """Get Unifi Access Lockdown Switch status.""" + return self.hub.lockdown + async def async_turn_off(self, **kwargs: Any) -> None: "Turn off Evacuation." await self.hass.async_add_executor_job( @@ -122,6 +151,16 @@ async def async_turn_on(self, **kwargs: Any) -> None: ) def _handle_coordinator_update(self) -> None: - """Handle Unifi Access Door Lock updates from coordinator.""" + """Handle Unifi Access Lockdown Switch updates from coordinator.""" self._attr_is_on = self.hub.lockdown self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Add Unifi Access Lockdown Switch to Home Assistant.""" + await super().async_added_to_hass() + self.hub.register_callback(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Remove Unifi Access Lockdown Switch from Home Assistant.""" + await super().async_will_remove_from_hass() + self.hub.remove_callback(self.async_write_ha_state) From 362df4106ad33510993f4df56178fcb3a0d8da83 Mon Sep 17 00:00:00 2001 From: imhotep Date: Sun, 25 Aug 2024 13:32:37 -0700 Subject: [PATCH 14/16] adding github actions --- .github/workflows/hacs.yaml | 17 +++++++++++++++++ .github/workflows/hassfest.yaml | 14 ++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 .github/workflows/hacs.yaml create mode 100644 .github/workflows/hassfest.yaml diff --git a/.github/workflows/hacs.yaml b/.github/workflows/hacs.yaml new file mode 100644 index 0000000..8369e39 --- /dev/null +++ b/.github/workflows/hacs.yaml @@ -0,0 +1,17 @@ +name: HACS Action + +on: + push: + pull_request: + schedule: + - cron: "0 0 * * *" + +jobs: + hacs: + name: HACS Action + runs-on: "ubuntu-latest" + steps: + - name: HACS Action + uses: "hacs/action@main" + with: + category: "integration" \ No newline at end of file diff --git a/.github/workflows/hassfest.yaml b/.github/workflows/hassfest.yaml new file mode 100644 index 0000000..0a1bd9c --- /dev/null +++ b/.github/workflows/hassfest.yaml @@ -0,0 +1,14 @@ +name: Validate with hassfest + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + validate: + runs-on: "ubuntu-latest" + steps: + - uses: "actions/checkout@v4" + - uses: "home-assistant/actions/hassfest@master" \ No newline at end of file From defc57fd743d3ee8764834dd26398ce5d3b8ea4f Mon Sep 17 00:00:00 2001 From: imhotep Date: Sun, 25 Aug 2024 13:38:22 -0700 Subject: [PATCH 15/16] sort manifest alphabetically --- custom_components/unifi_access/manifest.json | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/custom_components/unifi_access/manifest.json b/custom_components/unifi_access/manifest.json index 4e79329..7450b48 100644 --- a/custom_components/unifi_access/manifest.json +++ b/custom_components/unifi_access/manifest.json @@ -1,12 +1,16 @@ { - "domain": "unifi_access", - "name": "Unifi Access", - "codeowners": ["@imhotep"], + "codeowners": [ + "@imhotep" + ], "config_flow": true, "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/unifi_access", - "issue_tracker": "https://github.com/imhotep/hass-unifi-access/issues", + "domain": "unifi_access", "iot_class": "local_push", - "version": "1.2.0", - "requirements": ["websocket-client==1.8.0"] + "issue_tracker": "https://github.com/imhotep/hass-unifi-access/issues", + "name": "Unifi Access", + "requirements": [ + "websocket-client==1.8.0" + ], + "version": "1.2.0" } \ No newline at end of file From 9b6afb3a9c433c5da9565c985d35bdbdc18bcd1d Mon Sep 17 00:00:00 2001 From: imhotep Date: Sun, 25 Aug 2024 13:42:32 -0700 Subject: [PATCH 16/16] sort manifest alphabetically --- custom_components/unifi_access/manifest.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/unifi_access/manifest.json b/custom_components/unifi_access/manifest.json index 7450b48..0eee97e 100644 --- a/custom_components/unifi_access/manifest.json +++ b/custom_components/unifi_access/manifest.json @@ -1,14 +1,14 @@ { + "domain": "unifi_access", + "name": "Unifi Access", "codeowners": [ "@imhotep" ], "config_flow": true, "dependencies": [], "documentation": "https://www.home-assistant.io/integrations/unifi_access", - "domain": "unifi_access", "iot_class": "local_push", "issue_tracker": "https://github.com/imhotep/hass-unifi-access/issues", - "name": "Unifi Access", "requirements": [ "websocket-client==1.8.0" ],