Add mmWave area commands and reports for Inovelli VZM32-SN - #4944
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #4944 +/- ##
==========================================
- Coverage 93.07% 92.60% -0.48%
==========================================
Files 401 424 +23
Lines 13306 14667 +1361
==========================================
+ Hits 12385 13582 +1197
- Misses 921 1085 +164 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds ZHA-side definitions for the Inovelli VZM32-SN mmWave (0xFC32) area configuration commands and device reports so ZHA can configure/read mmWave zones and parse live target/area reporting payloads.
Changes:
- Introduces
MMWaveAreaandMMWaveTargetZigpyStructs to model area bounds and live target reports. - Extends
InovelliVZM32SNMMWaveClusterwith new server commands for setting interference/detection/stay areas and new client (device→coordinator) report commands. - Adds unit tests validating round-trip serialize/deserialize of the new command payload schemas, including variable-length target reporting.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
zhaquirks/inovelli/types.py |
Adds new Zigpy Struct types used by mmWave command/report schemas. |
zhaquirks/inovelli/__init__.py |
Extends the VZM32-SN mmWave cluster with new control IDs and command/report definitions. |
tests/test_inovelli_blue.py |
Adds coverage for serialization/deserialization of the new mmWave commands/reports. |
The VZM32-SN mmWave cluster (0xFC32) exposes three area-definition
commands (interference, detection, stay) and five device-to-coordinator
reports that were previously only defined in zigbee-herdsman-converters'
inovelli.ts. This commit brings them to zha-device-handlers:
zhaquirks/inovelli/types.py
- MMWaveArea struct: x/y/z min/max bounds in mm
- MMWaveTarget struct: x, y, z, doppler, id for live target reports
zhaquirks/inovelli/__init__.py
- MMWaveControlId: add Reset_detection_area (0x04) and
Clear_stay_areas (0x05) to match the full device command set
- InovelliVZM32SNMMWaveCluster.ServerCommandDefs: add set_interference_area
(0x01), set_detection_area (0x02), set_stay_area (0x03). Docstring notes
the community-reported v1.00 set_stay_area xMin/xMax swap+negate bug
and the pre-compensation workaround.
- InovelliVZM32SNMMWaveCluster.ClientCommandDefs: new class with the five
device-originated reports. anyone_in_reporting_area (0x00) for per-area
occupancy, report_target_info (0x01) for live position streams using
t.List[MMWaveTarget], and report_interference_area / report_detection_area
/ report_stay_area (0x02-0x04) as readback responses to
mmwave_control_command(Obtain_areas).
Command names are snake_case per zha-device-handlers convention; payload
layouts match zigbee-herdsman-converters.
tests/test_inovelli_blue.py
- Round-trip tests for set_stay_area, report_stay_area, report_target_info
(variable-length), and anyone_in_reporting_area.
- ServerCommandDefs docstring: clarify the area_id indexing convention rather than treating it as inconsistent. Wire-level area_id is 0-indexed (0..3) per inovelli.ts, while report payloads use area_1..area_4 field names to match Inovelli's user-facing 1-indexed labeling. Document the mapping explicitly so callers don't read it as an off-by-one bug. - test_vzm32_mmwave_report_target_info_variable_length: drop the schema-introspection trick that grabbed the compiled list type from report_target.fields. zigpy's Struct coerces a plain Python list into the t.List[MMWaveTarget] field, so the test no longer reaches into zigpy internals.
9c854e4 to
20aaa66
Compare
| def test_vzm32_mmwave_set_stay_area_roundtrip(): | ||
| """set_stay_area (server command 0x03) serializes bounds as little-endian int16s.""" | ||
| set_stay = InovelliVZM32SNMMWaveCluster.ServerCommandDefs.set_stay_area.with_compiled_schema().schema | ||
| payload = set_stay( | ||
| area_id=1, | ||
| x_min=-18, | ||
| x_max=600, | ||
| y_min=0, | ||
| y_max=425, | ||
| z_min=-116, | ||
| z_max=135, | ||
| ) | ||
| raw = payload.serialize() | ||
| # area_id(1) | x_min(-18=EEFF) | x_max(600=5802) | y_min(0=0000) | ||
| # | y_max(425=A901) | z_min(-116=8CFF) | z_max(135=8700) | ||
| assert raw == bytes.fromhex("01eeff58020000a9018cff8700") | ||
|
|
||
| parsed, rest = set_stay.deserialize(raw) | ||
| assert rest == b"" | ||
| assert parsed.area_id == 1 | ||
| assert parsed.x_min == -18 | ||
| assert parsed.x_max == 600 | ||
| assert parsed.z_max == 135 |
There was a problem hiding this comment.
We don't really need all these tests as this somewhat just tests zigpy itself but it's also fine to keep them for now.
zigpy-review-bot
left a comment
There was a problem hiding this comment.
Thanks for this — it's a careful, well-documented addition, and the command IDs, parameter order, and area/report payload layouts all match Z2M's inovelli.ts for cluster 0xFC32. One wire-format issue to resolve before this lands, plus a couple of coordination notes.
Must address
MMWaveTarget.target_idisint16s, but the device appears to encode the target id as a singleint8byte. Z2M'sreport_target_infoconverter reads each target as a 9-byte record —x/y/z/dopasint16plusidasint8(stride = 9, commented "Per Inovelli cluster docs"). Withtarget_id: int16sthe struct is 10 bytes, so a real multi-targetreport_target_infoframe will misalign after the first target and either fail to deserialize or decode garbage. The four roundtrip tests can't catch this because they serialize and deserialize with the same struct (self-consistent, not wire-validated). Please confirm against a realreport_target_infocapture (needs themmwave_target_info_reportattribute 0x006B enabled) and, if confirmed, changetarget_idtoint8s.
Please also
- This branch is based on
devfrom before the 2.0 quirks-API refactor (#5113). A plain merge of currentdevis conflict-free here (I verified locally — thezhaquirks.clustersimport refactor resolves automatically and your new code is already 2.0-compatible), so just mergedevto bring it current.
Coordination
- Two other open PRs touch the VZM32-SN surface: #5014 (adds attribute-based config entities, including mmWave zone-bound numbers on the same 0xFC32 cluster) and #5122 (ports the 0xFC31 config entities to quirks v2). This PR is complementary — it adds area commands/reports, not entities — but a maintainer should sequence the three so they merge cleanly.
| y: types.int16s | ||
| z: types.int16s | ||
| dop: types.int16s | ||
| target_id: types.int16s |
There was a problem hiding this comment.
target_id should very likely be types.int8s, not int16s. Z2M's report_target_info converter in inovelli.ts reads each target as a 9-byte record (x/y/z/dop int16 + id int8, stride = 9, "Per Inovelli cluster docs"). At int16s this struct is 10 bytes, so multi-target reports misalign after the first target. Please verify against a real report_target_info capture (attribute 0x006B mmwave_target_info_report must be enabled) — the roundtrip test can't catch this since it uses the same struct on both sides.
Each target in a report_target_info frame is a 9-byte record (x/y/z/dop as int16 plus id as int8, per the Inovelli cluster docs and Z2M's converter stride). With int16s the struct was 10 bytes, misaligning every target after the first in multi-target reports. Pin the exact wire layout in the test so the format is no longer roundtrip-only.
|
On TheJulianJES's behalf: pushed a small wire-format fix to this branch — Per the Inovelli cluster docs (and matching Zigbee2MQTT's converter, which reads |
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
Summary
The VZM32-SN mmWave cluster (0xFC32) exposes three area-definition commands
(interference, detection, stay) and five device-to-coordinator reports that
are currently only defined in zigbee-herdsman-converters' inovelli.ts.
This PR brings them to zha-device-handlers so ZHA users can configure and
read back mmWave zones natively.
What's added
zhaquirks/inovelli/types.py— two structs:MMWaveArea— x/y/z min/max bounds in mmMMWaveTarget— x, y, z, doppler, id for live target reportszhaquirks/inovelli/__init__.py, inInovelliVZM32SNMMWaveCluster:MMWaveControlIdenum: addReset_detection_area = 0x04andClear_stay_areas = 0x05.ServerCommandDefs: addset_interference_area(0x01),set_detection_area(0x02),set_stay_area(0x03).ClientCommandDefs(new):anyone_in_reporting_area(0x00),report_target_info(0x01, variable-lengtht.List[MMWaveTarget]),report_interference_area(0x02),report_detection_area(0x03),report_stay_area(0x04).Command names are snake_case per the project's convention; payload layouts
match
inovelli.ts.A note on
set_stay_areaon v1.00The docstring records a community-reported bug
on main MCU firmware v1.00 where
set_stay_area'sx_min/x_maxareswapped and sign-inverted on write. Pre-compensation (send
-b, -ato landon
(a, b)) is documented as a workaround; symmetric ranges[-n, +n]areself-correcting. Status on v1.01/v1.02 betas is not publicly documented.
This is a device-firmware issue, not something this quirk can fix — the
note is there so callers know why asymmetric x-axis stay zones may need
pre-compensation.
Test plan
python -m pytest tests/test_inovelli_blue.py -vpasses (1 existing + 4 new tests)python -m ruff check zhaquirks/inovelli/ tests/test_inovelli_blue.py— cleanpython -m ruff format --check zhaquirks/inovelli/ tests/test_inovelli_blue.py— cleanpython -m pytest tests/— 4746 pass on this branch (3 pre-existing Tuya/time_machine failures on Python 3.14, unrelated)