Skip to content

Add mmWave area commands and reports for Inovelli VZM32-SN - #4944

Merged
TheJulianJES merged 3 commits into
zigpy:devfrom
tmmueller:inovelli-vzm32-area-commands
Jul 29, 2026
Merged

Add mmWave area commands and reports for Inovelli VZM32-SN#4944
TheJulianJES merged 3 commits into
zigpy:devfrom
tmmueller:inovelli-vzm32-area-commands

Conversation

@tmmueller

Copy link
Copy Markdown
Contributor

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 mm
  • MMWaveTarget — x, y, z, doppler, id for live target reports

zhaquirks/inovelli/__init__.py, in InovelliVZM32SNMMWaveCluster:

  • MMWaveControlId enum: add Reset_detection_area = 0x04 and
    Clear_stay_areas = 0x05.
  • ServerCommandDefs: add set_interference_area (0x01),
    set_detection_area (0x02), set_stay_area (0x03).
  • ClientCommandDefs (new): anyone_in_reporting_area (0x00),
    report_target_info (0x01, variable-length t.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_area on v1.00

The docstring records a community-reported bug
on main MCU firmware v1.00 where set_stay_area's x_min/x_max are
swapped and sign-inverted on write. Pre-compensation (send -b, -a to land
on (a, b)) is documented as a workaround; symmetric ranges [-n, +n] are
self-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 -v passes (1 existing + 4 new tests)
  • python -m ruff check zhaquirks/inovelli/ tests/test_inovelli_blue.py — clean
  • python -m ruff format --check zhaquirks/inovelli/ tests/test_inovelli_blue.py — clean
  • Full suite python -m pytest tests/ — 4746 pass on this branch (3 pre-existing Tuya/time_machine failures on Python 3.14, unrelated)
  • Round-trip verified against real VZM32-SN devices on firmware v1.00 (set_stay_area writes succeed; no regression in existing mmWave attribute reads)

@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.60%. Comparing base (69bdc15) to head (5052ba8).
⚠️ Report is 42 commits behind head on dev.

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.
📢 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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 MMWaveArea and MMWaveTarget Zigpy Structs to model area bounds and live target reports.
  • Extends InovelliVZM32SNMMWaveCluster with 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.

Comment thread zhaquirks/inovelli/__init__.py Outdated
Comment thread tests/test_inovelli_blue.py Outdated
tmmueller added 2 commits May 9, 2026 08:21
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.
@tmmueller
tmmueller force-pushed the inovelli-vzm32-area-commands branch from 9c854e4 to 20aaa66 Compare May 9, 2026 13:25
@zigpy-review-bot zigpy-review-bot added bot: needs changes PR needs changes per LLM bot: 2.0 migration needed PR needs ZHA/quirks 2.0.0 migration per LLM enhancement Improve an existing quirk bot: needs maintainer PR does something questionable that needs a maintainer decision per LLM bot: small PR PR is small per LLM labels Jul 18, 2026
@zigpy-review-bot zigpy-review-bot changed the title Inovelli VZM32-SN: add mmWave area commands and reports Add mmWave area commands and reports for Inovelli VZM32-SN Jul 18, 2026
Comment on lines +69 to +91
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

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.

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.

@TheJulianJES TheJulianJES added the bot-trigger: post review Pending/new review is posted some time after this label is applied label Jul 29, 2026

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

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.

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_id is int16s, but the device appears to encode the target id as a single int8 byte. Z2M's report_target_info converter reads each target as a 9-byte record — x/y/z/dop as int16 plus id as int8 (stride = 9, commented "Per Inovelli cluster docs"). With target_id: int16s the struct is 10 bytes, so a real multi-target report_target_info frame 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 real report_target_info capture (needs the mmwave_target_info_report attribute 0x006B enabled) and, if confirmed, change target_id to int8s.

Please also

  • This branch is based on dev from before the 2.0 quirks-API refactor (#5113). A plain merge of current dev is conflict-free here (I verified locally — the zhaquirks.clusters import refactor resolves automatically and your new code is already 2.0-compatible), so just merge dev to 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.

Comment thread zhaquirks/inovelli/types.py Outdated
y: types.int16s
z: types.int16s
dop: types.int16s
target_id: types.int16s

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.

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.

@zigpy-review-bot zigpy-review-bot removed the bot-trigger: post review Pending/new review is posted some time after this label is applied label Jul 29, 2026
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.
@zigpy-review-bot

zigpy-review-bot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

On TheJulianJES's behalf: pushed a small wire-format fix to this branch — MMWaveTarget.target_id is now int8s instead of int16s.

Per the Inovelli cluster docs (and matching Zigbee2MQTT's converter, which reads report_target_info with an explicit 9-byte stride: x/y/z/dop as int16 + id as int8), each target record is 9 bytes. At int16s the struct was 10 bytes, so multi-target report frames would misalign after the first target. The roundtrip test couldn't catch this since it serialized and parsed with the same struct, so the test now also pins the exact expected wire bytes.

@TheJulianJES TheJulianJES left a comment

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.

Thanks!

@TheJulianJES
TheJulianJES merged commit b139f81 into zigpy:dev Jul 29, 2026
8 of 9 checks passed
FerrumLogic pushed a commit to FerrumLogic/zha-device-handlers that referenced this pull request Jul 29, 2026
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot: needs changes PR needs changes per LLM bot: needs maintainer PR does something questionable that needs a maintainer decision per LLM bot: small PR PR is small per LLM bot: 2.0 migration needed PR needs ZHA/quirks 2.0.0 migration per LLM enhancement Improve an existing quirk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants