Skip to content

Commit

Permalink
Merge pull request #49 from imhotep/evacuation_lock_down_temp_lock_rules
Browse files Browse the repository at this point in the history
Adding support for evacuation/lock down, temporary lock rules, initial support for UGT and UAH-Ent.
  • Loading branch information
imhotep authored Aug 25, 2024
2 parents 201330e + 9b6afb3 commit 19deac9
Show file tree
Hide file tree
Showing 16 changed files with 961 additions and 103 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/hacs.yaml
Original file line number Diff line number Diff line change
@@ -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"
14 changes: 14 additions & 0 deletions .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
@@ -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"
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# 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)
- Unifi Access Ultra :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 Log.

# Installation (HACS)
- Add this repository as a custom repository in HACS and install the integration.
Expand Down Expand Up @@ -56,6 +64,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 `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

```
Expand Down
18 changes: 17 additions & 1 deletion custom_components/unifi_access/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
"""The Unifi Access integration."""

from __future__ import annotations

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant

from .const import DOMAIN
from .coordinator import UnifiAccessCoordinator
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:
Expand All @@ -19,6 +29,12 @@ 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
Expand Down
11 changes: 4 additions & 7 deletions custom_components/unifi_access/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -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))
Expand All @@ -44,16 +42,16 @@ 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."""
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
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"

Expand All @@ -63,7 +61,7 @@ def device_info(self) -> DeviceInfo:
return DeviceInfo(
identifiers={(DOMAIN, self.door.id)},
name=self.door.name,
model="UAH",
model=self.door.hub_type,
manufacturer="Unifi",
)

Expand Down Expand Up @@ -97,7 +95,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
Expand Down
2 changes: 2 additions & 0 deletions custom_components/unifi_access/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
29 changes: 29 additions & 0 deletions custom_components/unifi_access/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -40,3 +41,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
22 changes: 21 additions & 1 deletion custom_components/unifi_access/door.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand All @@ -26,11 +28,15 @@ def __init__(
self._is_locking = False
self._is_unlocking = False
self._hub = hub
self.hub_type = "UAH"
self._id = door_id
self.name = name
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:
Expand All @@ -50,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("Relay status not set - assuming locked")
return self.door_lock_relay_status == "lock"

@property
Expand All @@ -76,8 +85,19 @@ 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."""
"""Register callback, called when door changes state."""
self._callbacks.add(callback)

def remove_callback(self, callback: Callable[[], None]) -> None:
Expand Down
20 changes: 10 additions & 10 deletions custom_components/unifi_access/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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()),
Expand All @@ -48,13 +43,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:
Expand Down Expand Up @@ -88,22 +86,24 @@ 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.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"
self._attr_translation_placeholders = {"door_name": self.door.name}

@property
def device_info(self) -> DeviceInfo:
"""Get device information."""
return DeviceInfo(
identifiers={(DOMAIN, self.door.id)},
name=self.door.name,
model="UAH",
model=self.door.hub_type,
manufacturer="Unifi",
)

Expand Down
Loading

0 comments on commit 19deac9

Please sign in to comment.