Skip to content

Add an API-key-only connection mode to UniFi Protect - #176410

Merged
joostlek merged 55 commits into
home-assistant:devfrom
RaHehl:unifiprotect-public-only-config-mode
Aug 26, 2026
Merged

joostlek merged 55 commits into
home-assistant:devfrom
RaHehl:unifiprotect-public-only-config-mode

Conversation

@RaHehl

@RaHehl RaHehl commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor

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

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

Copilot AI balanced review requested due to automatic review settings July 13, 2026 13:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uiprotect and 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.

Comment thread homeassistant/components/unifiprotect/__init__.py Outdated
Comment thread homeassistant/components/unifiprotect/config_flow.py
Comment thread homeassistant/components/unifiprotect/config_flow.py Outdated
Copilot AI review requested due to automatic review settings July 13, 2026 14:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 24 changed files in this pull request and generated 3 comments.

Comment thread homeassistant/components/unifiprotect/const.py
Comment thread tests/components/unifiprotect/conftest.py
Comment thread tests/components/unifiprotect/test_config_flow.py
RaHehl added 2 commits August 25, 2026 14:46
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.
Copilot AI review requested due to automatic review settings August 25, 2026 12:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
Copilot AI review requested due to automatic review settings August 25, 2026 13:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.

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.
Copilot AI review requested due to automatic review settings August 25, 2026 13:31

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

Comment thread homeassistant/components/unifiprotect/data.py
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.
Copilot AI review requested due to automatic review settings August 25, 2026 13:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_PLATFORMS forwards 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,

@RaHehl
RaHehl marked this pull request as ready for review August 25, 2026 13:56

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

Comment thread homeassistant/components/unifiprotect/config_flow.py
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.lights and reduces pb.arm_mode to 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()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants