Keep a small slider where the finger left it while the light fades towards it (0.2.71)
A light group's lamps fade, Home Assistant reports every step, and the tile's slider followed each one: after a drag it jumped back to the old value and crept up. The Tile now holds the sent value (brightness, fan percent, volume) until Home Assistant reports within 3 % of it, reports the entity off, refuses, or reports once and stays quiet for 1.5 s; never longer than 8 s, and without any report the hold ends with the wait. A cover's position slider keeps following the blind. Firmware 0.2.60; host checks 17/17 on both boards. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,11 @@ struct Tile {
|
||||
float edit_value = NAN; uint32_t edit_since = 0; bool edit_sent = false;
|
||||
// Knob position a toggle shows while its command is under way.
|
||||
bool optimistic_on = false;
|
||||
// A slider the finger let go stays where it was put while the light fades towards it (firmware 0.2.60+): the value
|
||||
// sent, in the attribute's own unit (brightness 0-255, a fan's percent, a volume 0-1), the value Home Assistant
|
||||
// reported meanwhile, and when the last of those came.
|
||||
float slider_sent = NAN, slider_real = NAN;
|
||||
uint32_t slider_sent_at = 0, slider_state_at = 0;
|
||||
// A tap that switched this tile is waiting; `optimistic_prev_on` is the stand to put back on a refusal.
|
||||
bool optimistic_tap = false, optimistic_prev_on = false;
|
||||
// A sensor's graph: 24 samples over `history_hours`, empty without one.
|
||||
@@ -184,6 +189,49 @@ struct Tile {
|
||||
// message always wins, because it clears the flag in `observe`.
|
||||
void optimistic(bool on) { optimistic_prev_on = state == "on"; optimistic_on = on; optimistic_tap = true; state = on ? "on" : "off"; }
|
||||
void undo_optimistic() { if (optimistic_tap) { state = optimistic_prev_on ? "on" : "off"; optimistic_tap = false; } }
|
||||
// The attribute a small slider sets: nothing for a cover, whose position slider follows the blind as it moves.
|
||||
float *slider_field() {
|
||||
auto d = domain();
|
||||
return d == "light" ? &brightness : d == "fan" ? &percentage : d == "media_player" ? &volume : nullptr;
|
||||
}
|
||||
// Holding a slider: from the send until Home Assistant reports a value within 3 % of it, reports the entity off or
|
||||
// unavailable, refuses, or reports once and then stays quiet for SLIDER_SETTLE (a fan that only knows 33/66/100
|
||||
// took the nearest step). A hold never outlives SLIDER_HOLD_CAP, and with no report at all it ends with the wait.
|
||||
static constexpr uint32_t SLIDER_HOLD_CAP = 8000, SLIDER_SETTLE = 1500;
|
||||
bool slider_holding(uint32_t now) const {
|
||||
if (!std::isfinite(slider_sent) || refused_at || now - slider_sent_at >= SLIDER_HOLD_CAP) return false;
|
||||
if (!slider_state_at) return waiting(now) || answered_at;
|
||||
return now - slider_state_at < SLIDER_SETTLE;
|
||||
}
|
||||
float slider_span() const { return domain() == "light" ? 255 : domain() == "media_player" ? 1 : 100; }
|
||||
// The finger let go: the field shows the value sent from now on.
|
||||
void hold_slider(uint32_t now, float value) {
|
||||
float *field = slider_field();
|
||||
if (!field) return;
|
||||
slider_sent = value; slider_real = *field; slider_sent_at = now; slider_state_at = 0; *field = value;
|
||||
// A slider on an off light or fan turns it on, so the tile lights up with it, as after a tap.
|
||||
if (domain() != "media_player" && state == "off") optimistic(true);
|
||||
}
|
||||
// A state came in with `field` already parsed: keep the sent value in front while the hold goes on.
|
||||
void slider_reported(uint32_t now) {
|
||||
float *field = slider_field();
|
||||
if (!field || !std::isfinite(slider_sent)) return;
|
||||
float real = *field;
|
||||
bool moved = !std::isfinite(slider_real) ? std::isfinite(real) : std::isfinite(real) && std::fabs(real - slider_real) > 0.5f * slider_span() / 100;
|
||||
slider_real = real;
|
||||
bool reached = std::isfinite(real) && std::fabs(real - slider_sent) <= 3 * slider_span() / 100;
|
||||
bool off = !slider_active();
|
||||
if (reached || off || refused_at) { slider_sent = NAN; return; }
|
||||
if (moved) slider_state_at = std::max<uint32_t>(1, now);
|
||||
if (!slider_holding(now)) { slider_sent = NAN; return; }
|
||||
*field = slider_sent;
|
||||
}
|
||||
// The hold ran out without a report that ended it: what Home Assistant last said shows again.
|
||||
void release_slider() {
|
||||
float *field = slider_field();
|
||||
if (field && std::isfinite(slider_sent)) *field = slider_real;
|
||||
slider_sent = NAN;
|
||||
}
|
||||
std::string domain() const { return entity.substr(0, entity.find('.')); }
|
||||
bool builtin() const { return domain() == "screen"; }
|
||||
// Two built-in cards, and only one of them is a clock that has to be redrawn every minute.
|
||||
|
||||
@@ -536,6 +536,7 @@ inline std::string receive(const std::string &payload) {
|
||||
tile.unit = string(a["unit_of_measurement"], 20);
|
||||
tile.brightness = number(a["brightness"]);
|
||||
tile.percentage = number(a["percentage"]);
|
||||
tile.slider_reported(esphome::millis());
|
||||
tile.position = number(a["current_position"]);
|
||||
next.tilt = number(a["current_tilt_position"]);
|
||||
tile.current = number(a["current_temperature"]);
|
||||
@@ -663,6 +664,7 @@ inline void watch_call(esphome::api::HomeassistantActionRequest &request, const
|
||||
for (size_t i = 0; i < model.tiles.size(); ++i) if (model.tiles[i].entity == entity) {
|
||||
model.tiles[i].pending = false;
|
||||
model.tiles[i].undo_optimistic();
|
||||
model.tiles[i].release_slider();
|
||||
model.tiles[i].refused_at = std::max<uint32_t>(1, esphome::millis());
|
||||
refresh_tile(i);
|
||||
}
|
||||
@@ -812,10 +814,11 @@ inline void commit_slider(unsigned i,int raw){
|
||||
if(i>=model.count || !fresh())return;auto &t=model.tiles[i];if(!t.available() || t.waiting(esphome::millis()))return;
|
||||
float value=std::clamp(raw,0,1000)/1000.0f;auto d=t.domain();
|
||||
// A light's slider stops at 1 %, as in Home Assistant; tapping the card turns it off.
|
||||
if(d=="light")action("light.turn_on",t.entity,"brightness",std::to_string(std::max(3,(int)std::lround(value*255))));
|
||||
if(d=="fan")action("fan.set_percentage",t.entity,"percentage",std::to_string((int)std::lround(value*100)));
|
||||
// The slider stays where the finger left it while the light fades towards it (Tile::hold_slider).
|
||||
if(d=="light"){int sent=std::max(3,(int)std::lround(value*255));action("light.turn_on",t.entity,"brightness",std::to_string(sent));t.hold_slider(esphome::millis(),sent);}
|
||||
if(d=="fan"){int sent=(int)std::lround(value*100);action("fan.set_percentage",t.entity,"percentage",std::to_string(sent));t.hold_slider(esphome::millis(),sent);}
|
||||
if(d=="cover")action("cover.set_cover_position",t.entity,"position",std::to_string((int)std::lround(value*100)));
|
||||
if(d=="media_player")action("media_player.volume_set",t.entity,"volume_level",std::to_string(value));
|
||||
if(d=="media_player"){action("media_player.volume_set",t.entity,"volume_level",std::to_string(value));t.hold_slider(esphome::millis(),value);}
|
||||
if(d=="number"||d=="input_number") {
|
||||
if(!std::isfinite(t.minimum)||!std::isfinite(t.maximum)||t.maximum<=t.minimum||t.step<=0)return;
|
||||
value=std::clamp(t.minimum+std::round(value*(t.maximum-t.minimum)/t.step)*t.step,t.minimum,t.maximum);
|
||||
@@ -3198,6 +3201,8 @@ inline void tick() {
|
||||
for(size_t i=0;i<model.tiles.size();++i){
|
||||
auto &t=model.tiles[i];
|
||||
if(t.refused_at && esphome::millis()-t.refused_at>=4000){t.refused_at=0;card(i);}
|
||||
// A held slider whose light never got there shows what Home Assistant last reported again.
|
||||
if(std::isfinite(t.slider_sent) && !t.slider_holding(esphome::millis())){t.release_slider();card(i);}
|
||||
if(!t.pending || t.waiting(esphome::millis()))continue;
|
||||
// A vacuum chip Home Assistant never confirmed goes back to what the robot reports.
|
||||
bool sent=false;if(auto *x=t.extra_ptr())for(auto &c:x->choices)sent=sent||!c.sent.empty();
|
||||
|
||||
Reference in New Issue
Block a user