Describe the bug
AttributeCache.set_value() unconditionally removes the attribute's unsupported marker before writing the value (zigpy/zcl/helpers.py):
def set_value(self, attr_def, value, *, last_updated=None):
self.remove_unsupported(attr_def)
...
The zigbee.db restore path writes every persisted attribute row back through this method at startup (zigpy/appdb.py, cluster._attr_cache.set_value(...)).
Together these mean a quirk that declares an attribute unsupported at cluster instantiation — the documented add_unsupported_attribute() pattern, used by many quirks to tell consumers "this device will never provide this attribute" — has that declaration silently erased on every startup whenever a stale cached value for the attribute exists in the database. Stale values are easy to acquire: any earlier quirk version (or the bare cluster) that once populated the attribute leaves rows behind, and quirk swaps don't purge them.
Clearing the marker on set_value() is sound semantics for live traffic — a successful read or report is genuine evidence the device supports the attribute. The database restore is not new evidence, though: it just replays history, possibly from a different quirk's era, and it happens after the current quirk's clusters have been instantiated and have made their declarations.
Impact (downstream, zha)
zha's entity discovery gates sensor creation on Cluster.is_attribute_unsupported(). With the mark wiped by the restore, zha creates entities for attributes the active quirk explicitly declared unavailable. Nothing ever feeds them, so users get permanently-"Unknown" entities that cannot be deleted (the integration keeps providing them) and reappear after every restart.
Concrete case: the Tuya PJ-1203A quirk in zigpy/zha-device-handlers#4498 marks Metering.instantaneous_demand and several ElectricalMeasurement attributes unsupported. Users migrating from an earlier custom quirk that populated those attributes get zombie "Instantaneous demand" / "Power factor" sensors on every start (HA 2026.8.1: zigpy 2.1.0 / zha 2.1.0 / zha-quirks 2.2.0). Reproduced end-to-end with the zha test harness: a fresh join is clean; the same join with pre-seeded cache values recreates every zombie.
Minimal reproduction (pure zigpy 2.1.0)
from unittest.mock import MagicMock
from zigpy.zcl.clusters.smartenergy import Metering
cluster = Metering(MagicMock())
attr = Metering.AttributeDefs.instantaneous_demand
cluster.add_unsupported_attribute(attr.id)
assert cluster.is_attribute_unsupported(attr.name) # True — quirk's declaration
# What the zigbee.db attribute restore does on startup (zigpy/appdb.py):
cluster._attr_cache.set_value(attr, 1234)
assert not cluster.is_attribute_unsupported(attr.name) # mark silently erased
Expected behavior
An unsupported mark set programmatically on the current cluster instance (i.e. by the active quirk) should survive the persistence restore. Values written by live reads/reports clearing the mark is fine.
Possible directions
- Give the restore path a way to write values without clearing marks (e.g.
set_value(..., clear_unsupported=False) used by appdb), or
- Skip restoring cached values for attributes currently marked unsupported on the instantiated (quirked) cluster, or
- Re-assert quirk-declared marks after the restore completes.
Any of these would also stop the stale rows from being re-persisted indefinitely.
Workaround
Quirk-side, on zha-quirks 2.x: suppress the affected default entities with prevent_default_entity_creation(...) (proposed for the PJ-1203A quirk in zigpy/zha-device-handlers#4498). This hides the symptom but doesn't restore the add_unsupported_attribute() contract for other quirks.
Environment
- zigpy 2.1.0 (Home Assistant 2026.8.1, zha 2.1.0, zha-quirks 2.2.0)
- Radio: n/a (persistence-layer behavior, reproducible without a radio)
Describe the bug
AttributeCache.set_value()unconditionally removes the attribute's unsupported marker before writing the value (zigpy/zcl/helpers.py):The
zigbee.dbrestore path writes every persisted attribute row back through this method at startup (zigpy/appdb.py,cluster._attr_cache.set_value(...)).Together these mean a quirk that declares an attribute unsupported at cluster instantiation — the documented
add_unsupported_attribute()pattern, used by many quirks to tell consumers "this device will never provide this attribute" — has that declaration silently erased on every startup whenever a stale cached value for the attribute exists in the database. Stale values are easy to acquire: any earlier quirk version (or the bare cluster) that once populated the attribute leaves rows behind, and quirk swaps don't purge them.Clearing the marker on
set_value()is sound semantics for live traffic — a successful read or report is genuine evidence the device supports the attribute. The database restore is not new evidence, though: it just replays history, possibly from a different quirk's era, and it happens after the current quirk's clusters have been instantiated and have made their declarations.Impact (downstream, zha)
zha's entity discovery gates sensor creation on
Cluster.is_attribute_unsupported(). With the mark wiped by the restore, zha creates entities for attributes the active quirk explicitly declared unavailable. Nothing ever feeds them, so users get permanently-"Unknown" entities that cannot be deleted (the integration keeps providing them) and reappear after every restart.Concrete case: the Tuya PJ-1203A quirk in zigpy/zha-device-handlers#4498 marks
Metering.instantaneous_demandand severalElectricalMeasurementattributes unsupported. Users migrating from an earlier custom quirk that populated those attributes get zombie "Instantaneous demand" / "Power factor" sensors on every start (HA 2026.8.1: zigpy 2.1.0 / zha 2.1.0 / zha-quirks 2.2.0). Reproduced end-to-end with the zha test harness: a fresh join is clean; the same join with pre-seeded cache values recreates every zombie.Minimal reproduction (pure zigpy 2.1.0)
Expected behavior
An unsupported mark set programmatically on the current cluster instance (i.e. by the active quirk) should survive the persistence restore. Values written by live reads/reports clearing the mark is fine.
Possible directions
set_value(..., clear_unsupported=False)used by appdb), orAny of these would also stop the stale rows from being re-persisted indefinitely.
Workaround
Quirk-side, on zha-quirks 2.x: suppress the affected default entities with
prevent_default_entity_creation(...)(proposed for the PJ-1203A quirk in zigpy/zha-device-handlers#4498). This hides the symptom but doesn't restore theadd_unsupported_attribute()contract for other quirks.Environment