Skip to content

Commit 19deac9

Browse files
authored
Merge pull request #49 from imhotep/evacuation_lock_down_temp_lock_rules
Adding support for evacuation/lock down, temporary lock rules, initial support for UGT and UAH-Ent.
2 parents 201330e + 9b6afb3 commit 19deac9

16 files changed

+961
-103
lines changed

.github/workflows/hacs.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: HACS Action
2+
3+
on:
4+
push:
5+
pull_request:
6+
schedule:
7+
- cron: "0 0 * * *"
8+
9+
jobs:
10+
hacs:
11+
name: HACS Action
12+
runs-on: "ubuntu-latest"
13+
steps:
14+
- name: HACS Action
15+
uses: "hacs/action@main"
16+
with:
17+
category: "integration"

.github/workflows/hassfest.yaml

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
name: Validate with hassfest
2+
3+
on:
4+
push:
5+
pull_request:
6+
schedule:
7+
- cron: '0 0 * * *'
8+
9+
jobs:
10+
validate:
11+
runs-on: "ubuntu-latest"
12+
steps:
13+
- uses: "actions/checkout@v4"
14+
- uses: "home-assistant/actions/hassfest@master"

README.md

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
# Unifi Access Custom Integration for Home Assistant
22

3-
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_.
3+
- This is a basic integration of [Unifi Access](https://ui.com/door-access) in [Home Assistant](https://homeassistant.io).
4+
- 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.
5+
- _Camera Feeds are currently not offered by the API and therefore **NOT** supported_.
6+
7+
# Supported hardware
8+
- Unifi Access Hub (UAH) :white_check_mark:
9+
- Unifi Access Hub Enterprise (UAH-Ent) :x: (partial/experimental support)
10+
- Unifi Gate Hub (UGT) :x: (partial/experimental support)
11+
- Unifi Access Ultra :x: (partial/experimental support)
412

513
# Getting Unifi Access API Token
614
- Log in to Unifi Access and Click on Security -> Advanced -> API Token
7-
- Create a new token and pick all permissions (this is *IMPORTANT*)
15+
- Create a new token and pick all permissions (this is *IMPORTANT*). At the very least pick: Space, Device and System Log.
816

917
# Installation (HACS)
1018
- Add this repository as a custom repository in HACS and install the integration.
@@ -56,6 +64,18 @@ An entity will get created for each door. Every time a door is accessed (entry,
5664
- actor # this is the name of the user that accessed the door. If set to N/A that means UNAUTHORIZED ACCESS!
5765
- type # `unifi_access_entry` or `unifi_access_exit`
5866

67+
### Evacuation/Lockdown
68+
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).
69+
70+
### Door lock rules (only applies to UAH)
71+
The following entities will be created: `input_select`, `input_number` and 2 `sensor` entities (end time and current rule).
72+
You are able to select one of the following rules via the `input_select`:
73+
- **keep_lock**: door is locked indefinitely
74+
- **keep_unlock**: door is unlocked indefinitely
75+
- **custom**: door is unlocked for a given interval (use the input_number to define how long. Default is 10 minutes).
76+
- **reset**: clear all lock rules
77+
- **lock_early**: locks the door if it's currently on an unlock schedule.
78+
5979
# Example automation
6080

6181
```

custom_components/unifi_access/__init__.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
"""The Unifi Access integration."""
2+
23
from __future__ import annotations
34

45
from homeassistant.config_entries import ConfigEntry
56
from homeassistant.const import Platform
67
from homeassistant.core import HomeAssistant
78

89
from .const import DOMAIN
10+
from .coordinator import UnifiAccessCoordinator
911
from .hub import UnifiAccessHub
1012

11-
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.EVENT]
13+
PLATFORMS: list[Platform] = [
14+
Platform.BINARY_SENSOR,
15+
Platform.EVENT,
16+
Platform.LOCK,
17+
Platform.NUMBER,
18+
Platform.SELECT,
19+
Platform.SENSOR,
20+
Platform.SWITCH,
21+
]
1222

1323

1424
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -19,6 +29,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
1929
hub.set_api_token(entry.data["api_token"])
2030
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = hub
2131

32+
coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub)
33+
34+
await coordinator.async_config_entry_first_refresh()
35+
36+
hass.data[DOMAIN]["coordinator"] = coordinator
37+
2238
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
2339

2440
return True

custom_components/unifi_access/binary_sensor.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from homeassistant.helpers.update_coordinator import CoordinatorEntity
1616

1717
from .const import DOMAIN
18-
from .coordinator import UnifiAccessCoordinator
1918
from .hub import UnifiAccessHub
2019

2120
_LOGGER = logging.getLogger(__name__)
@@ -29,9 +28,8 @@ async def async_setup_entry(
2928
"""Add Binary Sensor for passed config entry."""
3029
hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id]
3130

32-
coordinator = UnifiAccessCoordinator(hass, hub)
31+
coordinator = hass.data[DOMAIN]["coordinator"]
3332

34-
await coordinator.async_config_entry_first_refresh()
3533
binary_sensor_entities: list[UnifiDoorStatusEntity | UnifiDoorbellStatusEntity] = []
3634
for key in coordinator.data:
3735
binary_sensor_entities.append(UnifiDoorStatusEntity(coordinator, key))
@@ -44,16 +42,16 @@ class UnifiDoorStatusEntity(CoordinatorEntity, BinarySensorEntity):
4442
"""Unifi Access DPS Entity."""
4543

4644
should_poll = False
45+
_attr_translation_key = "access_door_dps"
46+
_attr_has_entity_name = True
4747

4848
def __init__(self, coordinator, door_id) -> None:
4949
"""Initialize DPS Entity."""
5050
super().__init__(coordinator, context=door_id)
5151
self._attr_device_class = BinarySensorDeviceClass.DOOR
52-
self.id = door_id
5352
self.door = self.coordinator.data[door_id]
5453
self._attr_unique_id = self.door.id
5554
self.device_name = self.door.name
56-
self._attr_name = f"{self.door.name} Door Position Sensor"
5755
self._attr_available = self.door.door_position_status is not None
5856
self._attr_is_on = self.door.door_position_status == "open"
5957

@@ -63,7 +61,7 @@ def device_info(self) -> DeviceInfo:
6361
return DeviceInfo(
6462
identifiers={(DOMAIN, self.door.id)},
6563
name=self.door.name,
66-
model="UAH",
64+
model=self.door.hub_type,
6765
manufacturer="Unifi",
6866
)
6967

@@ -97,7 +95,6 @@ def __init__(self, coordinator, door_id) -> None:
9795
"""Initialize Doorbell Entity."""
9896
super().__init__(coordinator, context=door_id)
9997
self._attr_device_class = BinarySensorDeviceClass.OCCUPANCY
100-
self.id = door_id
10198
self.door = self.coordinator.data[door_id]
10299
self._attr_unique_id = f"doorbell_{self.door.id}"
103100
self.device_name = self.door.name

custom_components/unifi_access/const.py

+2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
UNIFI_ACCESS_API_PORT = 12445
77
DOORS_URL = "/api/v1/developer/doors"
88
DOOR_UNLOCK_URL = "/api/v1/developer/doors/{door_id}/unlock"
9+
DOOR_LOCK_RULE_URL = "/api/v1/developer/doors/{door_id}/lock_rule"
910
DEVICE_NOTIFICATIONS_URL = "/api/v1/developer/devices/notifications"
11+
DOORS_EMERGENCY_URL = "/api/v1/developer/doors/settings/emergency"
1012

1113
DOORBELL_EVENT = "doorbell_press"
1214
DOORBELL_START_EVENT = "unifi_access_doorbell_start"

custom_components/unifi_access/coordinator.py

+29
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def __init__(self, hass: HomeAssistant, hub) -> None:
2727
hass,
2828
_LOGGER,
2929
name="Unifi Access Coordinator",
30+
always_update=False,
3031
update_interval=update_interval,
3132
)
3233
self.hub = hub
@@ -40,3 +41,31 @@ async def _async_update_data(self):
4041
raise ConfigEntryAuthFailed from err
4142
except ApiError as err:
4243
raise UpdateFailed("Error communicating with API") from err
44+
45+
46+
class UnifiAccessEvacuationAndLockdownSwitchCoordinator(DataUpdateCoordinator):
47+
"""Unifi Access Switch Coordinator."""
48+
49+
def __init__(self, hass: HomeAssistant, hub) -> None:
50+
"""Initialize Unifi Access Switch Coordinator."""
51+
update_interval = timedelta(seconds=3) if hub.use_polling is True else None
52+
53+
super().__init__(
54+
hass,
55+
_LOGGER,
56+
name="Unifi Access Evacuation and Lockdown Switch Coordinator",
57+
update_interval=update_interval,
58+
)
59+
self.hub = hub
60+
61+
async def _async_update_data(self):
62+
"""Handle Unifi Access Switch Coordinator updates."""
63+
try:
64+
async with asyncio.timeout(10):
65+
return await self.hass.async_add_executor_job(
66+
self.hub.get_doors_emergency_status
67+
)
68+
except ApiAuthError as err:
69+
raise ConfigEntryAuthFailed from err
70+
except ApiError as err:
71+
raise UpdateFailed("Error communicating with API") from err

custom_components/unifi_access/door.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ def __init__(
1515
name: str,
1616
door_position_status: str,
1717
door_lock_relay_status: str,
18+
door_lock_rule: str,
19+
door_lock_rule_ended_time: int,
1820
hub,
1921
) -> None:
2022
"""Initialize door."""
@@ -26,11 +28,15 @@ def __init__(
2628
self._is_locking = False
2729
self._is_unlocking = False
2830
self._hub = hub
31+
self.hub_type = "UAH"
2932
self._id = door_id
3033
self.name = name
3134
self.door_position_status = door_position_status
3235
self.door_lock_relay_status = door_lock_relay_status
3336
self.doorbell_request_id = None
37+
self.lock_rule = door_lock_rule
38+
self.lock_rule_interval = 10
39+
self.lock_rule_ended_time = door_lock_rule_ended_time
3440

3541
@property
3642
def doorbell_pressed(self) -> bool:
@@ -50,6 +56,9 @@ def is_open(self):
5056
@property
5157
def is_locked(self):
5258
"""Solely used for locked state when calling lock."""
59+
if self.door_lock_relay_status == "":
60+
self.door_lock_relay_status = "lock"
61+
_LOGGER.warning("Relay status not set - assuming locked")
5362
return self.door_lock_relay_status == "lock"
5463

5564
@property
@@ -76,8 +85,19 @@ def unlock(self) -> None:
7685
else:
7786
_LOGGER.error("Door with door ID %s is already unlocked", self.id)
7887

88+
def set_lock_rule(self, lock_rule_type) -> None:
89+
"""Set lock rule."""
90+
new_door_lock_rule = {"type": lock_rule_type}
91+
if lock_rule_type == "custom":
92+
new_door_lock_rule["interval"] = self.lock_rule_interval
93+
self._hub.set_door_lock_rule(self._id, new_door_lock_rule)
94+
95+
def get_lock_rule(self) -> None:
96+
"""Get lock rule."""
97+
self._hub.get_door_lock_rule(self._id)
98+
7999
def register_callback(self, callback: Callable[[], None]) -> None:
80-
"""Register callback, called when Roller changes state."""
100+
"""Register callback, called when door changes state."""
81101
self._callbacks.add(callback)
82102

83103
def remove_callback(self, callback: Callable[[], None]) -> None:

custom_components/unifi_access/event.py

+10-10
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@
1717
DOORBELL_START_EVENT,
1818
DOORBELL_STOP_EVENT,
1919
)
20-
from .coordinator import UnifiAccessCoordinator
2120
from .door import UnifiAccessDoor
22-
from .hub import UnifiAccessHub
2321

2422
_LOGGER = logging.getLogger(__name__)
2523

@@ -30,11 +28,8 @@ async def async_setup_entry(
3028
async_add_entities: AddEntitiesCallback,
3129
) -> None:
3230
"""Add Binary Sensor for passed config entry."""
33-
hub: UnifiAccessHub = hass.data[DOMAIN][config_entry.entry_id]
3431

35-
coordinator: UnifiAccessCoordinator = UnifiAccessCoordinator(hass, hub)
36-
37-
await coordinator.async_config_entry_first_refresh()
32+
coordinator = hass.data[DOMAIN]["coordinator"]
3833

3934
async_add_entities(
4035
(AccessEventEntity(hass, door) for door in coordinator.data.values()),
@@ -48,13 +43,16 @@ class AccessEventEntity(EventEntity):
4843
"""Authorized User Event Entity."""
4944

5045
_attr_event_types = [ACCESS_ENTRY_EVENT, ACCESS_EXIT_EVENT]
46+
_attr_translation_key = "access_event"
47+
_attr_has_entity_name = True
48+
should_poll = False
5149

5250
def __init__(self, hass: HomeAssistant, door) -> None:
5351
"""Initialize Unifi Access Door Lock."""
5452
self.hass = hass
5553
self.door: UnifiAccessDoor = door
5654
self._attr_unique_id = f"{self.door.id}_access"
57-
self._attr_name = f"{self.door.name} Access"
55+
self._attr_translation_placeholders = {"door_name": self.door.name}
5856

5957
@property
6058
def device_info(self) -> DeviceInfo:
@@ -88,22 +86,24 @@ class DoorbellPressedEventEntity(EventEntity):
8886

8987
_attr_device_class = EventDeviceClass.DOORBELL
9088
_attr_event_types = [DOORBELL_START_EVENT, DOORBELL_STOP_EVENT]
89+
_attr_translation_key = "doorbell_event"
90+
_attr_has_entity_name = True
91+
should_poll = False
9192

9293
def __init__(self, hass: HomeAssistant, door) -> None:
9394
"""Initialize Unifi Access Doorbell Event."""
9495
self.hass = hass
95-
self.id = door.id
9696
self.door: UnifiAccessDoor = door
9797
self._attr_unique_id = f"{self.door.id}_doorbell_press"
98-
self._attr_name = f"{self.door.name} Doorbell Press"
98+
self._attr_translation_placeholders = {"door_name": self.door.name}
9999

100100
@property
101101
def device_info(self) -> DeviceInfo:
102102
"""Get device information."""
103103
return DeviceInfo(
104104
identifiers={(DOMAIN, self.door.id)},
105105
name=self.door.name,
106-
model="UAH",
106+
model=self.door.hub_type,
107107
manufacturer="Unifi",
108108
)
109109

0 commit comments

Comments
 (0)