Add an API-key-only connection mode to UniFi Protect - #176410
Conversation
…only-config-mode # Conflicts: # homeassistant/components/unifiprotect/data.py
… flag for public-only
There was a problem hiding this comment.
Pull request overview
Adds API-key-only UniFi Protect connectivity with limited public-API-backed functionality.
Changes:
- Adds setup, reauthentication, discovery, and reconfiguration flows.
- Supports public-only cameras, alarms, diagnostics, and lifecycle handling.
- Updates
uiprotectand expands test coverage.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
homeassistant/components/unifiprotect/__init__.py |
Adds public-only setup and lifecycle handling. |
homeassistant/components/unifiprotect/alarm_control_panel.py |
Supports public NVR alarm entities. |
homeassistant/components/unifiprotect/config_flow.py |
Adds connection-mode flows. |
homeassistant/components/unifiprotect/const.py |
Defines public-only platforms. |
homeassistant/components/unifiprotect/data.py |
Handles public websocket state and refreshes. |
homeassistant/components/unifiprotect/diagnostics.py |
Adds redacted public diagnostics. |
homeassistant/components/unifiprotect/manifest.json |
Updates uiprotect. |
homeassistant/components/unifiprotect/media_source.py |
Excludes unsupported public-only entries. |
homeassistant/components/unifiprotect/migrate.py |
Makes migrations bootstrap-independent. |
homeassistant/components/unifiprotect/services.py |
Rejects unsupported public-only actions. |
homeassistant/components/unifiprotect/strings.json |
Adds flow text and errors. |
homeassistant/components/unifiprotect/utils.py |
Creates public-only clients. |
homeassistant/components/unifiprotect/views.py |
Rejects unsupported media requests. |
requirements_all.txt |
Updates the generated dependency pin. |
tests/components/unifiprotect/conftest.py |
Adds public-only fixtures. |
tests/components/unifiprotect/test_config_flow.py |
Tests new configuration flows. |
tests/components/unifiprotect/test_init.py |
Tests hybrid websocket reauthentication. |
tests/components/unifiprotect/test_media_source.py |
Updates client mocks. |
tests/components/unifiprotect/test_public_only.py |
Tests public-only behavior end to end. |
Platform.LIGHT is forwarded in public-only mode, but the API-key-only client mock only primed public_bootstrap.cameras. Light setup raised AttributeError on the missing lights map, which entity_platform swallows into a log line, so every public-only test passed with no light entity. Prime lights alongside cameras and resolve both families in all_devices() and get(), then exercise the real public-only setup path end to end.
Lights joined PUBLIC_ONLY_PLATFORMS, but every flow description still named only the alarm manager and cameras, and the platform test asserted a hardcoded pair that no longer matched what is forwarded. Name lights in the five mode descriptions and assert the forwarded set against PUBLIC_ONLY_PLATFORMS, so adding a platform fails the test until the user-facing scope is updated with it. Also drop the note about the pre-7.2 NVR mac backfill: 7.2.105 is the minimum, and it always carries the mac in the public bootstrap.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (1)
homeassistant/components/unifiprotect/const.py:88
- Align the advertised API-key-only scope with the implemented platform list. The PR description limits this slice to the alarm panel and cameras, but this line also forwards lights and the new flow text exposes them; either document lights as supported or remove them from this PR's API-key-only surface.
Platform.LIGHT,
Drops the Mock subclass in favour of a side_effect, as suggested in review. A PropertyMock cannot attach to a mock instance, so it goes on the instance type, which mock creates per Mock, leaving other mocks untouched.
The manual API-key step was the only setup path naming an entry after a host; every other path uses the NVR name. The name was already on the wire and discarded: resolve_nvr_mac() fetches GET /v1/nvrs in the flow, takes the mac and drops the rest. Fetch that endpoint directly and keep both fields. Network cost is unchanged, and the console fallback resolve_nvr_mac() falls back to is unreachable anyway, since the nvr schema marks mac required from the minimum supported version on and the version gate runs first.
ProtectData is kept when a setup never loaded, because HA only drops runtime_data after a successful unload. Its add-dedup baseline was taken against another snapshot, or in full-access mode, which leaves the mac set empty on purpose. Reusing it makes the first reconnect re-offer every enumerated device, and the platform rejects the duplicate unique id. Reset it where the client is swapped, and assert the public-only platform test stays free of platform setup errors: entity_platform turns those into a log line, so a forwarded platform can break silently.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
tests/components/unifiprotect/test_views.py:981
- Load a full-access entry before making this request so the proxy handler is actually exercised. Public-only setup never runs the
hass.http.register_view(...)calls in the full setup path, so this test currently receives aiohttp's route-level 404 and would still pass if_get_data_or_404()lost its new public-only guard.
await setup_public_only()
url = async_generate_thumbnail_url("test_id", ufp_public_only.entry.entry_id)
homeassistant/components/unifiprotect/diagnostics.py:37
- Include public lights in the API-key-only diagnostics payload.
PUBLIC_ONLY_PLATFORMSforwards the light platform, but this branch serializes only the NVR, cameras, and arm-mode presence, so diagnostics contain no data for supported public-only light entities.
public = {
"nvr": anonymize_data(pb.nvr.unifi_dict()) if pb.nvr is not None else None,
"cameras": [
async_redact_data(
cast(dict[str, Any], anonymize_data(camera.unifi_dict())),
TO_REDACT,
)
for camera in pb.cameras.values()
],
"arm_mode": pb.arm_mode is not None,
NotAuthorized subclasses ClientError, so the broad handler turned a rejected key into cannot_connect. resolve_nvr_mac() used to hide this by swallowing the error and falling through to the console, so it only became reachable when the flow started calling GET /v1/nvrs itself. Catch it first and attach it to the API-key field, like the meta-info validation above.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
homeassistant/components/unifiprotect/diagnostics.py:36
- Include the supported light and alarm-state payloads in public-only diagnostics. This branch claims to dump the public cache, but it omits
pb.lightsand reducespb.arm_modeto a presence boolean, leaving diagnostics unable to troubleshoot two of the three public-only platforms.
for camera in pb.cameras.values()
],
"arm_mode": pb.arm_mode is not None,
tests/components/unifiprotect/test_views.py:986
- Register the proxy route before asserting this 404. Public-only setup never reaches the full-access route registration at
homeassistant/components/unifiprotect/__init__.py:318-321, so this request returns a router-level 404 even if the new public-only guard is removed; load a full-access entry as well so the test exercises_get_data_or_404().
await setup_public_only()
Breaking change
Proposed change
Adds an API-key-only connection mode: the integration can be set up with just an API key from the UniFi OS Console, no local user. First user-reachable slice of the public-API migration (uilibs/uiprotect#929).
Scope is what the public Integration API fully backs today: the alarm manager panel, cameras (streams, snapshots, RTSP repair) and lights (on/off and brightness). Other platforms need a local user and are not forwarded; every mode description in the flow says so.
The user, reconfigure and discovery steps start with a mode menu. Reconfigure flips an entry in both directions, keeps stored local-user credentials, and unique IDs resolve the same in both modes, so a flip re-adopts the existing entities. A dedicated API-key reauth handles revoked keys.
Entry-wide consumers (diagnostics, media source, proxy views, device removal,
update_entity, actions) now handle entries without a private bootstrap, so a public-only entry cannot affect hybrid entries. Hybrid entries are otherwise unchanged.The public-only test mock raises on any private-bootstrap access, so accidental private reads fail in CI.
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: