-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add minimal UniFi Access integration #135139
base: dev
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems you haven't yet signed a CLA. Please do so here.
Once you do that we will be able to review and accept this pull request.
Thanks!
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
Hi! I've implemented a very bare bones UniFi Access lock integration based on the new(-ish) UniFi Access REST API. It can currently trigger an unlock and refreshes back to locked when notified by UniFi that the door has been reset back to locked. Locking is not implemented as unlocks in UniFi is mainly time based. I've had a few stumbles with how the integration code is intended to be structured, but for now I've taken note of the following that might need changes before merge:
Cheers, |
Since this is new, please use Thanks |
How about now? 🙂 |
bb0284a
to
49433c5
Compare
host = f"https://{entry.data.get(CONF_HOST)}/api/v1/developer" | ||
configuration = uiaccessclient.Configuration( | ||
host, access_token=entry.data.get(CONF_API_TOKEN) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have the endpoint here? I find the /api/v1/developer
url to be device/service specific and should be in the library IMO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. It is already in the library too, but was exposed awkwardly due to how I generated the client from OpenAPI. See the comment below about websocket. Will add a wrapper for configuration in the same way to fix this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now moved to library.
configuration = uiaccessclient.Configuration( | ||
host, access_token=entry.data.get(CONF_API_TOKEN) | ||
) | ||
configuration.verify_ssl = entry.data.get(CONF_VERIFY_SSL) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why isnt this a parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will fix with wrapper. See comment above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now moved to library.
configuration.verify_ssl = entry.data.get(CONF_VERIFY_SSL) | ||
api_client = uiaccessclient.ApiClient(configuration) | ||
|
||
door_coordinator = UniFiAccessDoorCoordinator(hass, api_client) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is it called door?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Door is terminology from the UniFi Access documentation. In reality this represents the accumulated state from a UniFi Access Hub, and any attached peripherals.
|
||
STEP_USER_DATA_SCHEMA = vol.Schema( | ||
{ | ||
vol.Required(CONF_HOST, default=DEFAULT_HOST): str, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So users host this themselves? What should the user enter? An url with port?
Because
- In that URL in
__init__.py
we assume that they have this hosting onhttps
, is that required for this kind of software? - Can they host it on different ports?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a service exposed by UniFi controllers. I might be wrong, but I don't think Ubiquity have made this available on any non-HTTPS endpoints. As far as I know you would need to have either a compatible UniFi controller on official Ubiquity hardware on premise where this is hosted, or connect to a cloud hosted UniFi controller. Either would be over HTTPS as far as I know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Little quick on the last one and forgot to answer as to the port, but as far as I can see that port is also fixed. I don't see any exposed configuration on my UniFi console to change it, and the documentation for the API seems to indicate that both HTTPS and port is fixed as is.
hass, | ||
_LOGGER, | ||
name="UniFi Access door", | ||
always_update=False, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why the always_update = False?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The models returned by the client are comparable and as such as far as I can tell "always_update" is not needed, as we can limit updates to whenever the state actually changes. Is this not the correct way of doing that?
async def receive_updated_data(self) -> None: | ||
"""Start websocket receiver for updated data from UniFi Access.""" | ||
_LOGGER.debug( | ||
"Starting UniFi Access websocket with %s", self.configuration.host | ||
) | ||
try: | ||
async with ( | ||
aiohttp.ClientSession( | ||
base_url=self.configuration.host.rstrip("/") + "/", | ||
headers={ | ||
"Authorization": f"Bearer {self.configuration.access_token}" | ||
}, | ||
) as session, | ||
session.ws_connect( | ||
"devices/notifications", verify_ssl=self.configuration.verify_ssl | ||
) as socket, | ||
): | ||
# WebSocket API is poorly documented so we will just use the REST API whenever we get | ||
# an update to fetch all the relevant data. | ||
async for message in socket: | ||
json = message.json() | ||
if ( | ||
type(json) is dict | ||
and json.get("event") == "access.data.v2.device.update" | ||
): | ||
_LOGGER.debug( | ||
"Received update from UniFi Access: %s", json.get("event") | ||
) | ||
self.async_set_updated_data( | ||
await self.hass.async_add_executor_job(self._update_data) | ||
) | ||
except Exception as exc: | ||
_LOGGER.error("Error in UniFi Access websocket receiver: %s", exc) | ||
raise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be abstracted away in the library
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, will add it to library. This is here as library as of now is just a generated OpenAPI client; and anything outside the scope of OpenAPI like websocket I've not yet implemented in the library. I'll add some extra custom endpoints for websocket to the library on top of the OpenAPI generated code to resolve this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My 2 cents, don't use an OpenAPI generated library. Yes it works quick and easy, but it will end you up with an integration that has to make up for the fact that the library is generated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is now moved to library.
@dataclass | ||
class UniFiAccessData: | ||
"""Data structure for UniFi Access integration.""" | ||
|
||
api_client: uiaccessclient.ApiClient | ||
door_coordinator: UniFiAccessDoorCoordinator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we store the api client here as we can just access it via the coordinator?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The coordinator carries the specific sub-client for the "space" sub-API, and not the general API client. I was thinking I might need to call multiple sub-APIs in some contexts, for example the the "space" and "device" sub-APIs to build support for devices in the entities. But maybe you guys have a structure where this should always come from the coordinators; and thus coordinated entries should never use clients not also used by their coordinator?
{ | ||
"domain": "unifi_access", | ||
"name": "UniFi Access", | ||
"codeowners": ["@hagen93"], | ||
"config_flow": true, | ||
"dependencies": [], | ||
"documentation": "https://www.home-assistant.io/integrations/unifi_access", | ||
"homekit": {}, | ||
"iot_class": "local_push", | ||
"requirements": ["uiaccessclient==0.9.1"], | ||
"ssdp": [], | ||
"zeroconf": [] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Quality scale should be set to bronze and the quality_scale.yaml should be evaluated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely. About evaluating the quality scale; do I manually evaluate each criteria, or has this been automated and will be generated by tooling?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A part is automated, but I strongly recommend reading the documentation to see what we do and why we do what we do
39881bd
to
1ff59d8
Compare
I'm unsure about the preferred procedure for reviews. Should I mark resolved threads myself, or do you prefer reviewers to confirm the resolution? 🙂 |
I usually have the following:
|
Great thanks. The ones I've fixed for now is simple typing issues and such, so I'll just resolve them then. 🙂 |
Please use the Ready for review button instead of requesting reviews directly |
Oh, so sorry. Not used to your conventions. 🙂 |
Proposed change
Adds very minimal initial support or UniFi Access (only lock entities).
Type of change
Additional information
Checklist
ruff format homeassistant tests
)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest
.requirements_all.txt
.Updated by running
python3 -m script.gen_requirements_all
.To help with the load of incoming pull requests: