Skip to content

fix(unifi-protect): stop keying device identity on DHCP host/IP#2082

Open
nberardi wants to merge 1 commit into
koush:mainfrom
nberardi:fix/unifi-protect-host-collapse
Open

fix(unifi-protect): stop keying device identity on DHCP host/IP#2082
nberardi wants to merge 1 commit into
koush:mainfrom
nberardi:fix/unifi-protect-host-collapse

Conversation

@nberardi

Copy link
Copy Markdown
Contributor

Problem

The Unifi Protect plugin was silently dropping cameras from the device list — a 13-camera NVR surfaced only 11 in Scrypted, and one device rendered an unrelated camera's stream under the wrong name.

Root cause is in getNativeId (plugins/unifi-protect/src/main.ts). When resolving a camera to its persistent Scrypted nativeId, the lookup fell back to matching on host (the camera's IP address):

const found = (mac && idMaps.mac?.[mac])
    || (anonymousDeviceId && idMaps.anonymousDeviceId?.[anonymousDeviceId])
    || (id && idMaps.id?.[id])
    || (host && idMaps.host?.[host]);   // <-- IP is not a stable identity

IPs are not stable. When DHCP recycles an address, a newly provisioned camera that picks up a previously-used IP resolves to the prior camera's nativeId. The subsequent cleanDict() pass then deletes the original camera's mac/anonymousDeviceId entries and rebinds them to the new camera — collapsing two distinct cameras onto a single Scrypted device and dropping the other from the device list entirely.

This was confirmed against a real affected install: three live camera IDs had all been mapped to one camera's nativeId in idToNativeId, and the displaced camera's MAC was missing from idMaps.mac (overwritten by cleanDict). The symptom — two missing cameras plus one device streaming the wrong feed under the wrong name — matched exactly.

Fix

Match device identity only on stable hardware identifiers: mac, anonymousDeviceId, and id. Stop using host as an identity key, and stop maintaining the idMaps.host map.

Notes

  • host remains in the getNativeId parameter type since callers still pass device objects that carry it; it is simply no longer used for matching.
  • Existing installs retain an orphaned idMaps.host in storage. It is now inert (nothing reads it) and harmless, so no migration is included. Affected installs self-heal on the next getNativeId write once the stale entry no longer participates in matching; a previously-collapsed device can be re-added via discovery.

getNativeId matched cameras on host (IP) as a fallback identity key. IPs
are not stable: when DHCP recycles an address, a newly provisioned camera
resolves to an unrelated camera's nativeId. cleanDict() then overwrites the
original camera's mac/anonymousDeviceId entries, collapsing two cameras onto
one Scrypted device and dropping the other from the device list.

Match only on stable hardware identifiers (mac, anonymousDeviceId, id) and
stop maintaining the host map.
@koush

koush commented Jun 30, 2026

Copy link
Copy Markdown
Owner

agreed that its are not stable but neither are these fwiw...

Match device identity only on stable hardware identifiers: mac, anonymousDeviceId, and id. Stop using host as an identity key, and stop maintaining the idMaps.host map.

@nberardi

Copy link
Copy Markdown
Contributor Author

You're right that "stable" was the wrong word, none of these are immutable. The bug isn't about stability, it's cross-device collision: cleanDict only collapses devices when two distinct devices map to the same key over time.

Field Source Two distinct devices share it over time?
mac hardware NIC No
anonymousDeviceId camera No
id NVR (on adoption) No
host DHCP Yes — recycled lease

mac/anonymousDeviceId/id can each change for a device (the recovery case the map handles), but host is the only one a new device inherits from an old one — camera B picks up the IP camera A just released. That aliasing drives the rebind that collapsed the NVR. Removing it isn't "more stable," it just drops the one key that collides across devices.

I can reword "stable", since that appears to be the wrong framing.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants