What happened?
A colour-temperature change never reconciles against its echo on any light whose integration stores mireds internally. The pending value stays authoritative for the full two-second settle window and then snaps to a slightly different number, instead of being confirmed by the state_changed that Home Assistant broadcasts a few hundred milliseconds after the call.
docs/adr/0002-pending-values-and-settle-window.md is explicit that the settle window exists for the failure case, not for the normal one:
Value matching alone never terminates when Home Assistant clamps the value, rejects the call, or silently drops it. Without the settle window the pending value would stay authoritative forever and the widget would quietly lie about the state of the house, which is the one failure mode a dashboard must not have.
Right now the normal path is the failure path for this axis.
Why
PendingValues compares an echo to the last value sent with a single absolute tolerance, EPSILON = 0.01 (src/app/pending.rs:45):
fn reconciles(&self, echoed: f32) -> bool {
self.last_sent
.is_some_and(|sent| (sent - echoed).abs() <= EPSILON)
}
The colour-temperature axis sends multiples of 50 K (step: 50.0, continuous_controls in src/ha/actions.rs). A light that works in mireds internally round-trips those through round(1_000_000 / kelvin) and back, which does not return the value we sent:
6350 K -> 157 mired -> 6369 K (19 K out)
6000 K -> 167 mired -> 5988 K (12 K out)
3000 K -> 333 mired -> 3003 K ( 3 K out)
2000 K -> 500 mired -> 2000 K ( exact )
Worst case over the grid Snapdash actually sends is 19 K, against a tolerance of 0.01. It can never match.
EPSILON is right for brightness and cover position, which are integers that make the round trip untouched. It is wrong the moment an axis passes through a unit conversion on the way back, and colour temperature is the first axis that does.
Steps to reproduce
- Pin a widget for a colour-temperature light whose integration stores mireds (most Zigbee and Hue bulbs; a template light backed by an
input_number will not reproduce this, because it stores kelvin verbatim).
- Expand the widget and drag the White slider to a value that does not round-trip cleanly, for example 6350 K.
- Release.
- The value stays local for two seconds, then jumps by up to 19 K when the settle window expires and Home Assistant truth is pushed back.
Expected: the echo arrives, matches, and hands control back to Home Assistant within a few hundred milliseconds, with no visible jump.
Proposed fix
Give the tolerance a per-axis value instead of one global constant, so an axis that round-trips through a conversion can declare how much slack that conversion costs it.
Suggested: 25 K for colour temperature (comfortably above the measured 19 K worst case and far below the 50 K step, so it can never confirm a neighbouring stop), and the current 0.01 retained for brightness, cover position and thermostat setpoint.
Notes
Found while planning #89, which needs the same seam for hue and saturation: those round-trip through 8-bit RGB and lose far more than colour temperature does. #89 will generalise this from a per-axis tolerance to a per-control one. This issue is the minimal fix and should land first, on its own, because it is a user-visible defect in what #87 already shipped and is independently testable.
Which area does this affect?
UI / widgets
What happened?
A colour-temperature change never reconciles against its echo on any light whose integration stores mireds internally. The pending value stays authoritative for the full two-second settle window and then snaps to a slightly different number, instead of being confirmed by the
state_changedthat Home Assistant broadcasts a few hundred milliseconds after the call.docs/adr/0002-pending-values-and-settle-window.mdis explicit that the settle window exists for the failure case, not for the normal one:Right now the normal path is the failure path for this axis.
Why
PendingValuescompares an echo to the last value sent with a single absolute tolerance,EPSILON = 0.01(src/app/pending.rs:45):The colour-temperature axis sends multiples of 50 K (
step: 50.0,continuous_controlsinsrc/ha/actions.rs). A light that works in mireds internally round-trips those throughround(1_000_000 / kelvin)and back, which does not return the value we sent:Worst case over the grid Snapdash actually sends is 19 K, against a tolerance of 0.01. It can never match.
EPSILONis right for brightness and cover position, which are integers that make the round trip untouched. It is wrong the moment an axis passes through a unit conversion on the way back, and colour temperature is the first axis that does.Steps to reproduce
input_numberwill not reproduce this, because it stores kelvin verbatim).Expected: the echo arrives, matches, and hands control back to Home Assistant within a few hundred milliseconds, with no visible jump.
Proposed fix
Give the tolerance a per-axis value instead of one global constant, so an axis that round-trips through a conversion can declare how much slack that conversion costs it.
Suggested: 25 K for colour temperature (comfortably above the measured 19 K worst case and far below the 50 K step, so it can never confirm a neighbouring stop), and the current 0.01 retained for brightness, cover position and thermostat setpoint.
Notes
Found while planning #89, which needs the same seam for hue and saturation: those round-trip through 8-bit RGB and lose far more than colour temperature does. #89 will generalise this from a per-axis tolerance to a per-control one. This issue is the minimal fix and should land first, on its own, because it is a user-visible defect in what #87 already shipped and is independently testable.
Which area does this affect?
UI / widgets