Repository navigation
Allow fractional metadata bounds - #1556
MartinHjelmare merged 8 commits into
Conversation
|
Done, moved the test to the event path. test_metadata_updated_fractional_bounds sends a metadata updated event with fractional bounds (min -12.8, max 12.7, default -5.5) through node.receive_event and asserts the value metadata picks them up. The pydantic unit test is gone. |
|
@AlCalzone can you confirm this is an appropriate change? I'm wondering why we haven't seen this need before. Is it related to this config change? |
zigpy-review-bot
left a comment
There was a problem hiding this comment.
The fix is correct and complete for the reported failure. MetaDataType is the one TypedDict behind every value's metadata, so widening it covers the events that carry metadata: the node ready and metadata updated events from #1555, and the controller's node added / node removed events, which embed the same node state. Against main's value.py the new test fails with three int_from_float errors (min, max and default).
Why this only shows up now
From the zwave-js source (AlCalzone may know more): zwave-js/zwave-js#9440 isn't the cause, but it is plausibly the trigger. It only adds an integer 4–28 °C range to the LC-13's Thermostat Setpoint value. Picking it up typically takes a re-interview, though (that is how the PR was tested), and the ready event after a re-interview carries the whole node state. That includes the Climate Control Schedule CC (0x46) overrideState value that fails here. That PR shipped in zwave-js 15.32.0 (2026-11-03), a week before #1555, which reports the failure after a re-interview.
The fractional bound itself is old: overrideState has had min: -12.8 since at least zwave-js v10. The change on this side is #1253 (0.66.0, first in Home Assistant 2025.8), which moved the event models from pydantic.v1 to pydantic v2. Under pydantic.v1 an int field silently truncates -12.8 to -12, and receive_event throws the validated model away, so nothing broke. Pydantic v2 rejects a fractional float for an int field (int_from_float).
overrideState is a static, auto-created value, so it is in the node state of every device that supports that CC. Only events are validated (ready, metadata updated, node added / node removed, …), not the state dump after connecting. In Home Assistant each failure reloads the integration, and the reload recovers until the next such event. That makes it easy to miss.
The fix also covers more than this one CC. Thermostat Setpoint and Humidity Control Setpoint take min / max from the device's capabilities report, decoded as floats. A device reporting a 4.5 °C minimum would fail the same way.
Heads-up for the Home Assistant bump
The widened return types make strict mypy fail in HA Core's zwave_js until the bump adapts them. In cover.py, _fully_open_position / _fully_closed_position (lines 127–136) and _fully_open_tilt / _fully_closed_tilt (lines 293–302) return metadata.max / metadata.min from -> int properties. In humidifier.py, min_humidity / max_humidity (lines 206–220) assign them to locals inferred as int. These are small casts in the bump PR, not a reason to change this PR: the types are now accurate.
What I checked
- The new test run against
main'svalue.py(3 ×int_from_float) and the PR's (passes);pytest test→ 404 passed;black --check,ruff check,mypy zwave_js_serverclean. pydantic.v1(bundled with pydantic 2.14): anOptional[int]field given-12.8→-12, no error.- zwave-js
origin/master:ClimateControlScheduleCC.tsoverrideStatemin: -12.8(also in v10.0.0), the only fractional static metadata bound inpackages/cc/packages/core. Static values default toautoCreate: true(Values.ts).ThermostatSetpointCC.ts/HumidityControlSetpointCC.tssetmin: this.minValue, max: this.maxValuefromparseFloatWithScale.CommandClasses.ts: "Climate Control Schedule" = 0x46, "Thermostat Setback" = 0x47. - zwave-js/zwave-js#9440: integer
min: 4/max: 28on the setpoint inlc-13.json/mt02650.json, listed in the v15.32.0 release notes. HA Corezwave_js/__init__.pyclient_listenlogs the exception and schedules a config entry reload. - This library:
Controller.__init__buildsNode(client, node_state)from the initial state without validation;Node.receive_eventand the controller's event handler validate. - Other numeric fields that stay
int(allowedentries, stringdefault): every zwave-js source ofallowedis integer-valued (configuration parameters, Sound Switch tone IDs, Wake Up interval), and no CC sets a non-numericdefault. They don't need to change here. - HA Core
devat b77233a923d: a strict-mypy probe of thecover.py/humidifier.pypatterns against this branch givesreturn-valueandassignmenterrors.
| # Z-Wave JS sends fractional bounds for some number values, eg the | ||
| # Thermostat Setback CC overrideState has a min of -12.8. Receiving | ||
| # the event failed while the bounds were typed as int. |
There was a problem hiding this comment.
Command class 70 (0x46) is the Climate Control Schedule CC; Thermostat Setback is 0x47. overrideState belongs to Climate Control Schedule, so the comment (and the PR description) should name that one. The last sentence describes the old code and can go:
| # Z-Wave JS sends fractional bounds for some number values, eg the | |
| # Thermostat Setback CC overrideState has a min of -12.8. Receiving | |
| # the event failed while the bounds were typed as int. | |
| # Z-Wave JS sends fractional bounds for some number values, eg the | |
| # Climate Control Schedule CC overrideState has a min of -12.8. |
|
Fixed the CC name in the test comment and the PR description, Climate Control Schedule not Thermostat Setback. Also brought in main (#1549), it had landed since and conflicted in test_node.py. Both tests kept. CI on the new head: builds, coverage and codecov/patch green, codecov/project still pending. The review bot note above covers the #9440 question. |
Some number values send fractional metadata bounds. The Climate Control Schedule CC
overrideStatehasmin: -12.8.MetaDataTypetypedminandmaxasint, so pydantic rejected the node events carrying them (int_from_float) and Home Assistant kept reconnecting.Widen
min,maxanddefaulttoint | floatinMetaDataTypeand in theValueMetadataproperties.defaultfails the same way with a fractional value, so it is widened too.Test:
test_metadata_updated_fractional_boundssends ametadata updatedevent with fractional bounds throughNode.receive_eventand checks the value metadata. Full suite: 404 passed.Closes #1555