Port Inovelli VZM entities from ZHA to quirks v2 - #5122
Conversation
Add InovelliQuirkBuilder with one method per Inovelli 0xFC31 config entity and wire the VZM30/31/32/35/36 device quirks to declare the entities that the ZHA library previously hard-coded. Each entity is created with a unique_id_suffix of "64561-<attribute>" (64561 == 0xFC31) so it keeps the unique_id ZHA produced for the old ZHA-native entity (which included the cluster id); the internal temperature sensor keeps ZHA's bare cluster-id suffix. The hard-coded entities must be removed from the ZHA library in the same release that bumps the zha-quirks pin to include this change.
1b4043d to
3d44273
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #5122 +/- ##
==========================================
+ Coverage 92.40% 92.56% +0.15%
==========================================
Files 416 424 +8
Lines 14314 14589 +275
==========================================
+ Hits 13227 13504 +277
+ Misses 1087 1085 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Other branch to also add entities for Inovelli VZM32-SN mmWave: dev...zigpy-bot/inovelli-vzm32-entities (just the second commit matters for that device – the other is this branch/PR). It's mostly based on Inovelli's official work:
|
Annotate the builder helpers and public entity methods with -> Self instead of a forward-referenced string / no annotation, matching the base QuirkBuilder and propagating correctly to any subclass.
zigpy-review-bot
left a comment
There was a problem hiding this comment.
Verdict: comment (positive) — reviewed on its merits as if ready, per an explicit request to ignore the draft status. I found no code defects; the port is verifiably complete and unique_id-safe. Leaving this as a comment rather than an approval since the head branch originates from the review bot's own earlier session — the final sign-off (and the coupled-release call with zigpy/zha#802) belongs to a human maintainer.
Summary of what was verified (programmatically, against the ZHA library's native Inovelli classes and the coupled zigpy/zha#802): entity-set parity per model (VZM30 38/38, VZM31 40/40, VZM32 37/37, VZM35 38/38, VZM36 ep1 17/17 + ep2 15/15), metadata parity (min/max, translation keys, entity categories, enum members), unique_id preservation (64561-<attr> suffixes throughout), a byte-identical regen round-trip against zha#802's snapshots, and that the interim state (quirks side released before zha#802) is safe.
Verification details
- Entity-set parity per model — computed the native set as {registered
0xFC31entity classes whosemodels=filter passes and whose attribute is defined on that model's quirked cluster} and matched it against the quirk declarations: VZM30 38/38, VZM31 40/40, VZM32 37/37, VZM35 38/38, VZM36 ep1 17/17 + ep2 15/15. No native entity is missing from the port, and no ported entity lacks a native counterpart. The per-model differences (noswitch_typeselect outside VZM31, fan variants only on VZM35, norelay_click_in_on_off_modeon VZM32/VZM35, VZM36's reduced per-endpoint sets) all fall out correctly from themodels=filters plus each cluster'sAttributeDefs. - Metadata parity — every number's min/max matches the native
_attr_native_min/max_value(no native class sets step/multiplier/unit/device_class), translation keys match (includingon_off_led_mode→one_led_mode), entity categories match (config everywhere; both sensors diagnostic), and all seven select/sensor enums reproduce the native member names and values exactly, so HA select options and existing translations carry over unchanged. - unique_id preservation — every entity carries
unique_id_suffix="64561-<attr>", and the internal temperature sensor's bare"64561"is correct: the nativeInovelliInternalTemperaturesets no_unique_id_suffix, so its unique_id was{ieee}-{ep}-64561. - Regen round-trip — staged zigpy/zha#802 in a zha worktree with this PR editable-installed and regenerated all six Inovelli snapshots: byte-identical to what #802 commits.
- Interim state is safe — regenerating against current zha dev (native entities still present) plus this PR produces no duplicate entities and no unique_id changes: where both exist, the native entity wins the
(platform, unique_id)slot and the v2 declaration fills in only the entities natives skipped. So releasing the quirks side first is fine; the only ordering constraint is that zha#802 must not ship before the zha-quirks pin includes this change.
One behavior delta worth stating in the PR body: ZHA-native config entities are skipped when the attribute has no cached value (_is_supported checks cluster.get(attr) is None), while quirks-v2 entities are created unconditionally. E.g. under zha#802 the old inovelli-vzm30-sn.json dump gains 24 config entities. This is effectively a fix — entities no longer vanish after a partial initial attribute read — and a firmware cross-check against Z2M indicates the unconditional creation is safe for exactly this entity set (no dead entities on old firmware):
Firmware-gating cross-check vs Z2M
- Z2M added per-model firmware gating for Inovelli attributes in Koenkk/zigbee-herdsman-converters#11883 (2026-04,
src/lib/inovelli.ts). The complete gate list: attribute-level gates exist only for the VZM31-SN —dimmingAlgorithm(P27, fw ≥ 3.05),auxDetectionLevel(P124, fw ≥ 3.05),dumbDetectionLevel(P165, fw ≥ 3.7); older firmware returnsUNSUPPORTED_ATTRIBUTEfor these (Koenkk/zigbee2mqtt#31616). Enum-value gates, also VZM31-only:switch_typevalue 3 ("Single-Pole Full Sine Wave") was removed in fw 3.0 andfanControlMode"Toggle" was added in fw 3.0. No gates exist for any VZM30/VZM32/VZM35/VZM36 attribute. - This PR declares no entities for the three gated attributes — they're
AttributeDefson the cluster only, same as ZHA-native (which also created no entities for them). So every unconditionally-created entity maps to an attribute Z2M exposes ungated on all firmware versions of that model. - The diagnostics dumps corroborate that the sparse attribute caches are dump vintage, not firmware capability: no Inovelli dump records any
unsupported_attributeson0xFC31; the old VZM30 dump lists only 17 of the cluster's attributes (15 cached — the ZHA version at dump time only read that subset), while the newer-firmware VZM30 dump (0x01100100) is even sparser (2 cached), and the VZM31 dump (fw0x01020212) has 37/44 cached. Z2M's VZM30outputModewas a converter omission fixed in Jul 2025 (Koenkk/zigbee-herdsman-converters#9596), not a firmware gate. - Pre-existing, not a regression from this port: the VZM31
InovelliSwitchTypeenum keepsSingle_Pole_Full_Sine = 0x03, which fw 3.0+ rejects withINVALID_VALUEper Z2M. Carried over verbatim from ZHA-native; ZHA has no firmware-conditional enum-value mechanism, so nothing actionable here.
Tests pass locally (3862 passed), pre-commit is clean on the changed files, and a Copilot (GPT-5.5) second opinion returned no findings. Open PRs #5014 and #4944 (VZM32 entity/command additions) touch the same files and will need rebasing onto the InovelliQuirkBuilder pattern once this lands.
…luster The entity count lock and the `64561-` unique_id suffix check both pass for a typo'd attribute name: the builder has to pass those names as string literals (no single Inovelli cluster defines all of them, so `AttributeDefs.x.name` references aren't practical), so a misspelled name still registers an entity with an intact suffix and an unchanged count, and only surfaces at runtime as a broken entity. Assert instead that every attribute-backed Inovelli entity names an attribute that actually exists on the cluster the quirk installs on that entity's endpoint. The cluster is derived from the quirk's own registered zigpy transforms (`.replaces()` per endpoint, `.replace_cluster_occurrences()` as the device-wide fallback) rather than a hand-written model -> class table, so the VZM36's separate light (endpoint 1) and fan (endpoint 2) clusters are each checked against their own attributes.
|
Behavior when this PR runs without the ZHA-side removal (zigpy/zha#802): benign — no duplicate entities, no errors, and the device presents exactly as it does on current ZHA. Both entity sources run during discovery: Consequences worth noting: Testing this PR as a custom quirk on stock ZHA does not exercise the v2 entitiesThe native classes shadow them all. The declarations only go live once zigpy/zha#802 removes the library copies. One asymmetry: quirk entities can appear where hidden native ones didn'tNative config entities gate on The hazard case is a unique_id mismatch, not a collisionAny v2 declaration whose suffix/endpoint doesn't exactly match its native twin would survive dedup alongside it, producing two visible entities for the same attribute. A snapshot regen against plain dev ZHA (without #802) doubles as a check — the VZM30/31/35 snapshots should come out unchanged. Net: the coupling requirement is one-directional. Shipping the quirks release first is safe (dormant declarations); only the ZHA-side removal must not ship without the quirks release, as stated in both PR bodies. |
There was a problem hiding this comment.
I'll clean this up later. For now, it's good enough that we can merge the quirks and builder 😅
| "output_mode", | ||
| InovelliOutputMode, |
There was a problem hiding this comment.
nit for future clean up: kwargs?
There was a problem hiding this comment.
Pull request overview
Ports Inovelli VZM-series entities that were previously created inside the ZHA library into quirks v2, using a shared InovelliQuirkBuilder so the per-device quirk files can declaratively register the same config entities while preserving legacy unique_id behavior.
Changes:
- Added
InovelliQuirkBuilder(zhaquirks/inovelli/builder.py) with one method per Inovelli0xFC31config entity, including explicitunique_id_suffixrules to preserve ZHA-native unique IDs. - Updated VZM30/31/32/35/36 quirk files to use
InovelliQuirkBuilderand declare the formerly hard-coded ZHA entities (including endpoint-specific declarations for VZM36). - Added
tests/test_inovelli.pyto validate entity counts, unique-id suffix conventions, and that declared attribute names exist on the installed Inovelli cluster per endpoint.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| zhaquirks/inovelli/VZM36.py | Switches VZM36 to InovelliQuirkBuilder and declares light/fan endpoint entities explicitly. |
| zhaquirks/inovelli/VZM35SN.py | Switches VZM35-SN to InovelliQuirkBuilder and declares the shared config entities. |
| zhaquirks/inovelli/VZM32SN.py | Switches VZM32-SN to InovelliQuirkBuilder and declares the shared config entities. |
| zhaquirks/inovelli/VZM31SN.py | Switches VZM31-SN to InovelliQuirkBuilder and declares the shared config entities. |
| zhaquirks/inovelli/VZM30SN.py | Switches VZM30-SN to InovelliQuirkBuilder and declares the shared config entities. |
| zhaquirks/inovelli/builder.py | Introduces the shared builder and associated enum types; centralizes entity metadata and unique-id preservation logic. |
| tests/test_inovelli.py | Adds regression tests for entity counts, unique-id suffix preservation, and attribute name correctness per endpoint cluster. |
| if "inovelli" not in str(entry.source.file).lower(): | ||
| continue | ||
| model = str(entry.source.file).split("/")[-1] |
There was a problem hiding this comment.
Yeah, I'll clean up this entire test file next. It's good enough to release an initial version with the ported quirks for HA beta and so we can port #5014.
There was a problem hiding this comment.
@zigpy-review-bot Create a "Task" issue, assign + tag me there, and mention what should be improved for the Inovelli quirks/tests. Also look through (comments in) the other two recently merged Inovelli PRs. List everything, possibly use /details blocks.
There was a problem hiding this comment.
Created #5218 (type: Task, assigned to you).
It collects the deferred items from this PR, #5014 and #4944: the tests/test_inovelli.py rewrite (with a note on which parts ZHA's snapshots now cover and which don't), the builder cleanups (the kwargs nit, missing -> Self on the VZM32-SN methods, and _inovelli_number() having no way to pass a unit — mmwave_hold_time is exposed with the raw uint32 max), the three 0xFC32 attributes that still have no entity (notably mmwave_target_info_report, without which #4944's report parsing can't be switched on from HA), the target_id int8s change that's still unverified against real hardware, and a section of known limitations for the record.
Not sure of theInovelliQuirkBuilderapproach yet. It does compact the actual Inovelli quirks a lot though, since many entities are shared.Proposed change
Add InovelliQuirkBuilder with one method per Inovelli 0xFC31 config entity and wire the VZM30/31/32/35/36 device quirks to declare the entities that the ZHA library previously hard-coded.
Each entity is created with a unique_id_suffix of "64561-" (64561 == 0xFC31) so it keeps the unique_id ZHA produced for the old ZHA-native entity (which included the cluster id); the internal temperature sensor keeps ZHA's bare cluster-id suffix.
The hard-coded entities must be removed from the ZHA library in the same release that bumps the zha-quirks pin to include this change.
Additional information
Related ZHA PR:
Device diagnostics
TO BE ADDED.
Checklist
pre-commitchecks pass / the code has been formatted using Black