Skip to content

Allow fractional metadata bounds - #1556

Merged
MartinHjelmare merged 8 commits into
home-assistant-libs:mainfrom
benjaminkincses:fix-fractional-metadata-bounds
Nov 11, 2026
Merged

MartinHjelmare merged 8 commits into
home-assistant-libs:mainfrom
benjaminkincses:fix-fractional-metadata-bounds

Conversation

@benjaminkincses

@benjaminkincses benjaminkincses commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Some number values send fractional metadata bounds. The Climate Control Schedule CC overrideState has min: -12.8.

MetaDataType typed min and max as int, so pydantic rejected the node events carrying them (int_from_float) and Home Assistant kept reconnecting.

Widen min, max and default to int | float in MetaDataType and in the ValueMetadata properties. default fails the same way with a fractional value, so it is widened too.

Test: test_metadata_updated_fractional_bounds sends a metadata updated event with fractional bounds through Node.receive_event and checks the value metadata. Full suite: 404 passed.

Closes #1555

Comment thread test/model/test_value.py Resolved
@benjaminkincses

Copy link
Copy Markdown
Contributor Author

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.

@MartinHjelmare

Copy link
Copy Markdown
Contributor

@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?

zwave-js/zwave-js#9440

@zigpy-review-bot zigpy-review-bot 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.

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's value.py (3 × int_from_float) and the PR's (passes); pytest test → 404 passed; black --check, ruff check, mypy zwave_js_server clean.
  • pydantic.v1 (bundled with pydantic 2.14): an Optional[int] field given -12.8 → -12, no error.
  • zwave-js origin/master: ClimateControlScheduleCC.ts overrideState min: -12.8 (also in v10.0.0), the only fractional static metadata bound in packages/cc / packages/core. Static values default to autoCreate: true (Values.ts). ThermostatSetpointCC.ts / HumidityControlSetpointCC.ts set min: this.minValue, max: this.maxValue from parseFloatWithScale. CommandClasses.ts: "Climate Control Schedule" = 0x46, "Thermostat Setback" = 0x47.
  • zwave-js/zwave-js#9440: integer min: 4 / max: 28 on the setpoint in lc-13.json / mt02650.json, listed in the v15.32.0 release notes. HA Core zwave_js/__init__.py client_listen logs the exception and schedules a config entry reload.
  • This library: Controller.__init__ builds Node(client, node_state) from the initial state without validation; Node.receive_event and the controller's event handler validate.
  • Other numeric fields that stay int (allowed entries, string default): every zwave-js source of allowed is integer-valued (configuration parameters, Sound Switch tone IDs, Wake Up interval), and no CC sets a non-numeric default. They don't need to change here.
  • HA Core dev at b77233a923d: a strict-mypy probe of the cover.py / humidifier.py patterns against this branch gives return-value and assignment errors.

Comment thread test/model/test_node.py Outdated
Comment on lines +1107 to +1109
# 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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:

Suggested change
# 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.

@AlCalzone AlCalzone 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.

Looks correct to me.

@benjaminkincses

Copy link
Copy Markdown
Contributor Author

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.

@MartinHjelmare MartinHjelmare left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks!

@MartinHjelmare
MartinHjelmare merged commit fde6d9f into home-assistant-libs:main Nov 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fractional metadata bounds cause event validation errors and Home Assistant reconnects

4 participants