Optimistic controls - #4
Conversation
|
@s3ansh33p Thank you for this PR! Very nice feature and improvements. Few comments:
Could you prepare these fixes? I would love to merge your PR. |
|
@konradk thanks for the feedback, have made relevant changes |
Expand a colour-capable light row with a hue/saturation wheel, the light's favourite colours, and a warmth slider for colour temperature. Capability comes from supported_color_modes, never a live hs_color: a colour light that is off reports none, which is exactly when the picker is wanted. Favourites come from the entity registry the panel already fetches, falling back to a port of the frontend's computeDefaultFavoriteColors so the swatches match the app's, and are normalized to hue/saturation or kelvin rather than carried as raw registry objects. Sliders and the wheel commit on release, as brightness does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A control cleared its drag value the moment the command went out, rebinding to the stale entity value for a full round trip: the slider showed the new value, snapped back, then jumped forward again when the state arrived. Each control now pins what was picked in a PendingValue until Model.*Settled matches it against the returned state, or a 6500ms expiry gives up. Every tolerance covers the round trip while staying strictly inside one slider step, so a state event still carrying the pre-command value can never count as confirmation. Brightness travels as a 0-255 byte: 0.5 of a percentage point absorbs the 0.39 that costs, within a step of 1. Colour temperature is compared in mireds, where a 50K step is 12 mireds at 2000K but only 1.17 at 6500K, so the slack is 1 mired — exactly Home Assistant's integer quantization, and still under the narrowest step. A light answers a hue in its own colour space, and an hs-xy-hs round trip loses hue accuracy in inverse proportion to saturation, so the hue tolerance is 500/saturation degrees: five at full saturation, the whole circle near the white centre where every angle is the same colour anyway. Both colour channels can be pending at once, since a wheel drag holds a colour without sending anything that would clear a pending warmth, so each PendingValue records when it was picked and the swatch highlighting follows the more recent one. The low/high climate band commits only the end that was touched. Both ends still go out together, because set_temperature rejects a partial range, and both are still read from what is on screen so the pair stays consistent with it. What changed is that the untouched end is no longer re-committed: an end the thermostat clamped away gets re-sent by a nudge to the other one, which is harmless, but its deadline is not pushed back, so it still expires on schedule and hands the display back to the device. Reconciliation runs in onEntityChanged rather than a readonly binding, which would loop: clearing the pending value changes what the binding reads. The signal fires because EntityRow rebuilds its `entity` property whenever Service bumps stateRevision, not because a new entity object arrived — EntityStore.upsertState mutates the map in place for an entity it already knows. The panel list is a Flickable and was stealing the grab whenever a drag strayed off-axis, cancelling it mid-gesture. preventStealing fixes the wheel; sliders need an overlay MouseArea since PanelSlider's own is private. The trade-off is that a drag starting on a slider or the wheel no longer scrolls the list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A drag read the pointer straight off the track, so a thermostat with a half-degree step could be sent 21.2. Only climate opts in: volume and warmth step by a UI nudge size, and quantizing a drag to that would leave the track with a couple of dozen reachable positions. Brightness rounds where it is sent instead, so the value held on screen is the one sent. The nudge buttons ask for a direction rather than the nearest point, so each lands on the step beside the target it started from. Rounding to nearest moved them by half a step one way and a step and a half the other, whenever the thermostat reported a target off its own grid. Both ends of a low/high band pin stepBase to the range minimum: those limits ride each other as either end is dragged, so the default base would move the grid under them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dropping the empty array made a cleared list indistinguishable from a light nobody had customised, so the computed defaults came back. A list whose entries no longer validate — temperatures kept from before the light stopped advertising color_temp — is the same choice, so it draws what survives and nothing else. Only an absent list takes the defaults. That leaves no room for a format we simply cannot read, so xy joins the colour forms that parse. The wheel already drives an xy light, and its saved favourites would now draw nothing at all rather than fall back to the defaults that used to cover the gap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tags were per-entity, so a failure could not be told from a newer call and a rejected value sat out its full 6500ms hold. Tags now carry a counter and commandFailed reports it, so only the value that call put on screen comes back down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
04518cd to
86bb4d8
Compare
|
There was a conflict with main because I merged another PR a few minutes earlier, so I rebased your branch and merged it. |
Follows on from #3 to have 'optimistic controls' in that when I use an input / slider, previously it was go to the desired value, then jump back to the previous value, then jump to the desired value again.
See video for context:
https://github.com/user-attachments/assets/96551a93-7967-4942-8b18-749a875e1da8