Skip to content

Add attribute_converter/value_converter to number entities - #851

Draft
TheJulianJES wants to merge 1 commit into
zigpy:devfrom
TheJulianJES:tjj/number-attribute-converter
Draft

Add attribute_converter/value_converter to number entities#851
TheJulianJES wants to merge 1 commit into
zigpy:devfrom
TheJulianJES:tjj/number-attribute-converter

Conversation

@TheJulianJES

@TheJulianJES TheJulianJES commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

DRAFT.

Q1: Can/should we even move quirks-initialized entities to zha-quirks itself?
Q2: Better naming?

Proposed change

Number entities are writable, so unlike sensors and binary sensors they need converters in both directions: attribute_converter maps the raw attribute value to the value shown in HA, value_converter maps the value from HA back to the raw attribute value.

As for sensors, a converter takes precedence over the multiplier.

Additional information

Linked quirks PR:

We need this for:

Number entities are writable, so unlike sensors and binary sensors they
need converters in both directions: attribute_converter maps the raw
attribute value to the value shown in HA, value_converter maps the value
from HA back to the raw attribute value.

As for sensors, a converter takes precedence over the multiplier.
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.27%. Comparing base (d7471c3) to head (db900ef).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev     #851   +/-   ##
=======================================
  Coverage   97.27%   97.27%           
=======================================
  Files          55       55           
  Lines       10941    10953   +12     
=======================================
+ Hits        10643    10655   +12     
  Misses        298      298           

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

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

Comment — draft PR, no blockers. The implementation mirrors the existing Sensor/BinarySensor converter precedent correctly as far as I could verify. Answers to Q1/Q2 below, plus three asks.

Re: Q1 — can/should quirks-initialized entities move to zha-quirks itself? (asked in the PR body)

Can: yes. The runtime dependency already points that way — zha-quirks declares dependencies = ["zigpy>=2.0.0", "zha>=2.0.0"], while zha carries zha-quirks only in its testing dependency-group — and zhaquirks/builder/discovery.py already owns metadata → entity construction. What could move is not NumberConfigurationEntity itself (zha's own config numbers subclass it) but quirk-only subclasses of it.

Should: I'd say no, at least not for this feature.

  • It widens the coupling instead of narrowing it. Today the seam is a small kwargs contract (_platform_kwargs__init__). A quirks-side subclass would instead inherit zha internals — _cluster, _multiplier, write_attributes_safe, maybe_emit_state_changed_event, _is_supported, the on_add event-subscription lifecycle. Refactors that are free today would become breaking changes for quirks.
  • It doesn't remove the lockstep it would be meant to remove. Overriding native_value/async_set_native_value quirks-side is a single-repo change only for behavior expressible on the existing zha base; anything needing a new hook still needs both repos. Meanwhile number-entity behavior ends up split across two repos, and the next reader has to know which half implements what.
  • Entity state shape and unique_id are what HA consumes, and the tests/data/devices/ snapshots that cover quirk entities live here. One repo, one source of truth.
  • This feature isn't quirk-specific anyway — a converter is a generic entity capability a native config number could equally want. That argues for exactly where you put it.

If the real motivation is the two-repo release dance, I'd attack that directly (forward-compatible kwarg handling on the zha side) rather than by relocating entity classes.

Re: Q2 — better naming? (asked in the PR body)

I'd keep attribute_converter / value_converter.

  • attribute_converter is already shipped public API on .sensor() and .binary_sensor(), so the read direction is effectively fixed — renaming it breaks existing quirks.
  • The pair is internally consistent under "each converter is named after what it consumes": attribute_converter takes the raw attribute, value_converter takes the HA value. And "value" is already ZHA's word for the HA side (native_value, async_set_native_value), so it's less vague here than it would be in isolation.
  • Alternatives I weighed: read_converter/write_converter is the clearest pair but requires renaming the shipped name; attribute_write_converter is unambiguous but asymmetric and verbose. Neither buys enough to justify the churn.

What the names genuinely don't carry is direction at the call site — but that's a docstring problem, not a naming problem. See the inline comment on the __init__ docstring.

Asks

  1. Mirror quirks#5215's both-or-neither validation on the zha side — the invariant belongs where the behavior is (inline).
  2. Document the converters, their direction, and their precedence over multiplier in the entity docstring (inline).
  3. The new tests bypass the quirks plumbing. Not fixable in this PR given the release ordering, but please don't lose the follow-up (inline).

Coupling / release ordering, for quirks#5215

zha#851 has to be released before quirks#5215 merges, and #5215 should bump its zha>= floor — it's still zha>=2.0.0 in that diff.

That's not cosmetic. BaseEntity.__init__(self, unique_id: str) takes no **kwargs, so attribute_converter= forwarded through NumberConfigurationEntityPlatformEntityBaseEntity raises TypeError against an older zha. And because Device wraps discover_entities() in a broad except Exception: … continue around a generator (zha/zigbee/device.py:1080-1088), that raise terminates the generator — so the device loses the offending entity and every remaining quirk v2 entity, with nothing but a log line. Pre-existing and out of scope here; noting it only because it makes the floor bump load-bearing.

Verified (8 checks)
  • tests/test_number.py — 12 passed in an isolated worktree venv (py3.14).
  • ./.venv/bin/python -m mypy zha/Success: no issues found in 57 source files. No regression.
  • Converter precedence matches Sensor.native_value (zha/application/platforms/sensor/__init__.py:352-354) and BinarySensor (binary_sensor/__init__.py:150-151): the converter short-circuits before divisor/multiplier/formatter. Consistent, as the PR body claims.
  • value_converter appears nowhere else in zhaquirks/zigpy — no name collision.
  • Read quirks#5215 in full: NumberMetadata fields, the __attrs_post_init__ both-or-neither ValueError, the _platform_kwargs plumbing, the builder kwargs, and the copilot-instructions docs.
  • t.uint8_t(3.5)3 against the installed zigpy 2.1.0 — a float write is silently truncated, not rejected. Basis for the inline note on async_set_native_value.
  • Dependency direction confirmed from both pyproject.tomls (quoted in the Q1 answer).
  • Second opinion from Copilot (GPT-5.5, --effort high), given both this diff and quirks#5215's diff: no findings.

if attribute_converter is not None:
self._attribute_converter = attribute_converter
if value_converter is not None:
self._value_converter = value_converter

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.

Please mirror quirks#5215's both-or-neither validation here.

NumberMetadata.__attrs_post_init__ raises ValueError when only one converter is given, but that guards only the quirks v2 entry point. This class is constructible directly, and a subclass can set _attribute_converter at class level without _value_converter. In either case native_value returns a converted value while async_set_native_value writes an unconverted one, so the round trip silently disagrees with itself.

The asymmetry is a property of this class's behavior, so the invariant reads better here — raise in __init__ when exactly one of the two ends up set, class-level defaults included — than only in the metadata layer.

@@ -278,6 +283,10 @@ def __init__(
"""Init this number configuration entity."""

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.

Please document the converters here: which direction each one goes, that they take precedence over multiplier, and that both are required together.

QuirkBuilder.number()'s docstring in quirks#5215 says all of this, but this is the class a reader lands on when they follow _platform_kwargs, and the parameter names alone don't carry direction.

self._cluster, {self._attribute_name: int(value / self._multiplier)}
)
if self._value_converter:
attr_value = self._value_converter(value)

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.

Worth documenting here: the default branch below forces int(value / self._multiplier), but the converter branch writes whatever the converter returns — and HA always hands async_set_native_value a float.

So a converter written the obvious way, lambda x: 5 - x rather than lambda x: 5 - int(x), produces a float, and zigpy truncates it silently rather than rejecting it (t.uint8_t(3.5)3, checked against zigpy 2.1.0). quirks#5215's prose does tell authors to cast, but nothing on either side enforces or warns.

A blanket int() here would be wrong — Single/Double attributes are legitimate — so the docstring is probably the right place. Just don't leave the warning only in the quirks-side docs.

*,
attribute_name: str | None = None,
attribute_converter: Callable[[Any], Any] | None = None,
value_converter: Callable[[Any], Any] | None = None,

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.

Nit: value_converter only ever receives the HA-side float from async_set_native_value, so Callable[[float], Any] would state the contract more precisely than Callable[[Any], Any]. (attribute_converter genuinely takes Any, so the asymmetry would be real rather than sloppy.) Low value — raising it only because this signature is what quirk authors read.

Comment thread tests/test_number.py
update_attribute_cache(cluster)
zha_device = await join_zigpy_device(zha_gateway, light)

entity = NumberConfigurationEntity(

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.

These tests construct the entity directly, which skips the plumbing the feature actually ships through: zhaquirks/builder/discovery.py::_platform_kwargs, i.e. exactly the lines quirks#5215 adds. Right now nothing in either repo covers that mapping — #5215's tests stop at NumberMetadata.

The precedent in this repo goes end to end: test_quirks_sensor_attr_converter (tests/test_sensor.py:1911) and test_quirks_binary_sensor_attr_converter (tests/test_binary_sensor.py:259) both build through QuirkBuilder(...).sensor(attribute_converter=...)registry.resolvejoin_zigpy_deviceget_entity.

I don't think that's achievable in this PR — QuirkBuilder.number() won't accept the kwargs until #5215 ships, and #5215 needs this released first — so keeping the direct-construction tests here is reasonable. Please add the QuirkBuilder-based test once the ordering allows (a zha follow-up, or in #5215 after the zha release), so the discovery mapping doesn't stay permanently untested.

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.

2 participants