Skip to content

Log unique ID conflicts#683

Draft
TheJulianJES wants to merge 2 commits into
zigpy:devfrom
TheJulianJES:tjj/unique_id_conflict_logging
Draft

Log unique ID conflicts#683
TheJulianJES wants to merge 2 commits into
zigpy:devfrom
TheJulianJES:tjj/unique_id_conflict_logging

Conversation

@TheJulianJES
Copy link
Copy Markdown
Contributor

This is experimental. It prints all unique ID conflicts (four times...)

These are existing ones:

zha.zigbee.device[69476] WARNING Entity unique_id conflict on device ab:cd:ef:12:63:c6:80:fe for (<Platform.UPDATE: 'update'>, 'ab:cd:ef:12:63:c6:80:fe-1-25-firmware_update'): keeping zha.application.platforms.update.FirmwareUpdateEntity (endpoint=1, cluster_handlers=['ota']), dropping zha.application.platforms.update.FirmwareUpdateServerEntity (endpoint=1, cluster_handlers=['ota'])
zha.zigbee.device[69476] WARNING Entity unique_id conflict on device 00:15:8d:00:02:af:97:27 for (<Platform.UPDATE: 'update'>, '00:15:8d:00:02:af:97:27-1-25-firmware_update'): keeping zha.application.platforms.update.FirmwareUpdateEntity (endpoint=1, cluster_handlers=['ota']), dropping zha.application.platforms.update.FirmwareUpdateServerEntity (endpoint=1, cluster_handlers=['ota'])
zha.zigbee.device[69476] WARNING Entity unique_id conflict on device 54:ef:44:10:00:14:f3:e7 for (<Platform.UPDATE: 'update'>, '54:ef:44:10:00:14:f3:e7-1-25-firmware_update'): keeping zha.application.platforms.update.FirmwareUpdateEntity (endpoint=1, cluster_handlers=['ota']), dropping zha.application.platforms.update.FirmwareUpdateServerEntity (endpoint=1, cluster_handlers=['ota'])
zha.zigbee.device[69476] WARNING Entity unique_id conflict on device 00:15:8d:00:02:54:cb:fa for (<Platform.UPDATE: 'update'>, '00:15:8d:00:02:54:cb:fa-1-25-firmware_update'): keeping zha.application.platforms.update.FirmwareUpdateEntity (endpoint=1, cluster_handlers=['ota']), dropping zha.application.platforms.update.FirmwareUpdateServerEntity (endpoint=1, cluster_handlers=['ota'])
zha.zigbee.device[69476] WARNING Entity unique_id conflict on device ab:cd:ef:12:7b:7c:3f:a7 for (<Platform.UPDATE: 'update'>, 'ab:cd:ef:12:7b:7c:3f:a7-232-25-firmware_update'): keeping zha.application.platforms.update.FirmwareUpdateEntity (endpoint=232, cluster_handlers=['ota']), dropping zha.application.platforms.update.FirmwareUpdateServerEntity (endpoint=232, cluster_handlers=['ota'])
zha.zigbee.device[69476] WARNING Entity unique_id conflict on device ab:cd:ef:12:c1:6d:31:24 for (<Platform.UPDATE: 'update'>, 'ab:cd:ef:12:c1:6d:31:24-1-25-firmware_update'): keeping zha.application.platforms.update.FirmwareUpdateEntity (endpoint=1, cluster_handlers=['ota']), dropping zha.application.platforms.update.FirmwareUpdateServerEntity (endpoint=1, cluster_handlers=['ota'])
zha.zigbee.device[69476] WARNING Entity unique_id conflict on device ab:cd:ef:12:35:1a:bf:9e for (<Platform.UPDATE: 'update'>, 'ab:cd:ef:12:35:1a:bf:9e-1-25-firmware_update'): keeping zha.application.platforms.update.FirmwareUpdateEntity (endpoint=1, cluster_handlers=['ota']), dropping zha.application.platforms.update.FirmwareUpdateServerEntity (endpoint=1, cluster_handlers=['ota'])

@codecov
Copy link
Copy Markdown

codecov Bot commented Feb 27, 2026

Codecov Report

❌ Patch coverage is 90.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.50%. Comparing base (229df22) to head (0aa985b).
⚠️ Report is 31 commits behind head on dev.

Files with missing lines Patch % Lines
zha/zigbee/device.py 90.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #683      +/-   ##
==========================================
- Coverage   97.51%   97.50%   -0.02%     
==========================================
  Files          62       62              
  Lines       10949    10969      +20     
==========================================
+ Hits        10677    10695      +18     
- Misses        272      274       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Collaborator

@zigpy-review-bot zigpy-review-bot left a comment

Choose a reason for hiding this comment

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

Draft, so this is mostly setup notes for when it comes out of WIP. The mechanic is right — useful diagnostic that surfaces a real bug (FirmwareUpdateEntity vs FirmwareUpdateServerEntity, both @register_entity(Ota.cluster_id) with _unique_id_suffix = "firmware_update" — they unconditionally collide on every OTA-capable device). The class-difference guard avoids spamming on benign same-class re-discovery, which is the right policy.

Logging level / call cardinality. _LOGGER.warning is correct for a real conflict. But your PR body shows each conflict logged ~4-7 times per device — that's the gateway re-driving async_initialize (cache=True initial, cache=False after online, re-interview, etc.) and _pending_entities never being reset between calls, so the same entity pair gets dedup'd again every pass. Two ways to handle:

  • Tag the conflict on self (self._logged_unique_id_conflicts: set[tuple[Platform, str]]) and check before logging, so a given key only fires once per device lifetime. Cheap, no behavior change.
  • Or accept multi-emission as evidence of the lifecycle bug — but then warning-level fires N times for the same problem and pollutes logs at scale.

I'd go with the per-device set. Keeps warning-level (correct), avoids the dup spam.

Logging-only vs reconcile. The PR title implies logging-only and the code matches — duplicates still exist (one is dropped via on_remove(), but the underlying registration duplication that produced them remains). Worth a one-liner in the PR body confirming this is intentional surface-the-bug logging, not a fix; the actual fix is presumably in @register_entity filtering (e.g. should FirmwareUpdateServerEntity use a different _unique_id_suffix, or not register on devices where FirmwareUpdateEntity already matches?).

Hot path. Runs once per async_initialize, only on dedup-collision. Not a hot path concern.

Type annotation nit. existing_entity/duplicate_entity are typed BaseEntity but the call site at 1060 passes from dict[tuple[Platform, str], PlatformEntity], and the new entity is also a PlatformEntity (it was just keyed off entity.unique_id + entity.PLATFORM after is_supported_in_list). The isinstance(entity, PlatformEntity) checks in _entity_endpoint_id / _entity_cluster_handler_names are dead branches today. Either narrow the param types to PlatformEntity and drop the isinstance guards, or leave the guards and document why (future-proofing for non-platform conflict checks). The current state is type-annotation overhead with no payoff.

Tests. None added. For a draft that's fine, but before ready-for-review: a small test asserting the warning fires with the expected fields would be cheap (mock two PlatformEntity subclasses with the same unique_id, run them through async_initialize, assert the log record). Codecov flagged 2 lines uncovered, presumably the early-return same-class branch — those are the lines worth testing.

Comment thread zha/zigbee/device.py
if existing_class == duplicate_class:
return

_LOGGER.warning(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

_LOGGER.warning is the right level for a real conflict, but with _pending_entities never reset between async_initialize calls (gateway drives it multiple times: cache=True initial, cache=False after online, re-interview), this fires per call for the same device. The PR body's 4-7 repeats per IEEE confirm it.

Suggestion: gate on a per-device set[tuple[Platform, str]] so each (platform, unique_id) conflict is logged once per device lifetime. Keeps warning-level (which is right — conflicts are bugs), without N-spamming the same finding.

# in __init__:
self._logged_unique_id_conflicts: set[tuple[Platform, str]] = set()

# in _log_entity_unique_id_conflict, after the class-equality check:
if key in self._logged_unique_id_conflicts:
    return
self._logged_unique_id_conflicts.add(key)

Comment thread zha/zigbee/device.py
self,
key: tuple[Platform, str],
existing_entity: BaseEntity,
duplicate_entity: BaseEntity,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

existing_entity/duplicate_entity typed as BaseEntity but the call site (line 1060) only ever passes PlatformEntitynew_entities is dict[tuple[Platform, str], PlatformEntity] and entity reached this point via entity.PLATFORM / entity.unique_id. The isinstance(entity, PlatformEntity) guards in _entity_endpoint_id / _entity_cluster_handler_names are dead branches.

Either narrow these to PlatformEntity and drop the guards (cleaner), or leave the guards in and add a comment explaining the future use case. Current shape is type-annotation overhead with no payoff.

Comment thread zha/zigbee/device.py

# Ignore entities that already exist
if key in new_entities:
self._log_entity_unique_id_conflict(key, new_entities[key], entity)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Worth confirming in the PR body: this is intentionally surface-the-bug logging, not a fix. The duplicate FirmwareUpdateServerEntity registration on Ota.cluster_id with the same _unique_id_suffix = "firmware_update" is the underlying issue; this PR just makes it visible. The actual fix presumably lives in giving the server entity a distinct suffix or scoping its @register_entity to coordinator devices only (since it's the OTA server side).

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