Grey out what Home Assistant calls off and colour the rest like Home Assistant, so an airco that is off no longer looks on (0.2.85)
The airco tile stayed orange while the airco was off. The firmware kept its own short list of what turns grey and gave an off climate Home Assistant's orange, but Home Assistant has no colour for a climate that is off: its cards fall back to the inactive grey. Other tiles had the same fault: an off fan, a closed blind, a docked robot, a player in standby, an idle script, a paused timer and a camera that isn't streaming all kept their colour. Tile::active() is now a port of the frontend's stateActive() for the domains a tile can show, and every tile that is not active is grey. An airco that is off reads Off, with the room's temperature when the device reports one, as Home Assistant's tile does. tile_controls::accent() follows stateColorCss(): alarm binary sensors red, a battery green, orange or red by its charge, a running script or timer and a streaming camera amber, the weather per condition, the sun amber and indigo, someone in a zone blue. Scenes, selects, numbers and sensors keep their own colour per kind. A full-page tile lights up only for tiles that can be off (Tile::lights_up), and a closed blind's slider keeps its colour. The top bar's alarm classes are Home Assistant's eleven. Checked with tools/check.sh (471 Python tests, 19/19 C++ with 58 state cases and every colour rule) and compiled for both boards: CYD 1,677,488 B (91.4 %, +576 B), Guition 2,054,640 B (25.3 %). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -290,20 +290,42 @@ struct Tile {
|
||||
auto d = domain();
|
||||
return state != "unknown" || d == "scene" || d == "button" || d == "input_button";
|
||||
}
|
||||
// Home Assistant's stateActive() (frontend src/common/entity/state_active.ts), the rule its cards colour by: an
|
||||
// entity it calls inactive is grey, such as an airco that is off, a closed blind, a docked robot, a player in
|
||||
// standby or a paused timer (firmware 0.2.71+). Only the cases of the domains a tile can show are here. A scene,
|
||||
// button or image states the moment it last ran (TIMESTAMP_STATE_DOMAINS), so only "unavailable" makes it
|
||||
// inactive. A built-in card has no state and is always active.
|
||||
bool active() const {
|
||||
return state == "on" || state == "cleaning" || state == "active" || (domain() == "person" && state == "home") ||
|
||||
(domain() == "sun" && state == "above_horizon") || (domain() == "climate" && available() && state != "off");
|
||||
if (builtin()) return true;
|
||||
if (!received || state.empty() || state == "unavailable") return false;
|
||||
auto d = domain();
|
||||
if (d == "scene" || d == "button" || d == "input_button" || d == "image") return true;
|
||||
if (state == "unknown" || state == "off") return false;
|
||||
if (d == "cover") return state != "closed";
|
||||
if (d == "person") return state != "not_home";
|
||||
if (d == "media_player") return state != "standby";
|
||||
if (d == "vacuum") return state != "idle" && state != "docked" && state != "paused";
|
||||
if (d == "timer") return state == "active";
|
||||
if (d == "camera") return state == "streaming" || state == "recording";
|
||||
return true;
|
||||
}
|
||||
// A slider shows the card's colour like Home Assistant's tile sliders: grey only while stateActive()
|
||||
// (frontend src/common/entity/state_active.ts) calls the entity inactive, such as an off light or fan and a
|
||||
// media player that is off or in standby. A number with a value is active there, and a closed cover's
|
||||
// position slider keeps the cover's colour so it never looks disabled (hui-cover-position-card-feature.ts).
|
||||
// A card over the whole page lights up in its state colour while it is on (firmware 0.2.62+): only a tile that can
|
||||
// be off, closed, docked or away, and only while active() calls it active (firmware 0.2.71+). Sensors, numbers,
|
||||
// selects, scenes, buttons, images, the weather and the sun are always active in Home Assistant and stay plain.
|
||||
bool lights_up() const {
|
||||
const auto d = domain();
|
||||
for (const char *on_off : {"light", "switch", "input_boolean", "climate", "fan", "cover", "media_player", "vacuum",
|
||||
"script", "timer", "camera", "person", "binary_sensor"})
|
||||
if (d == on_off) return active();
|
||||
return false;
|
||||
}
|
||||
// A slider shows the card's colour like Home Assistant's tile sliders: grey only while the entity is inactive
|
||||
// (active() above), such as an off light or fan and a media player that is off or in standby. A number with a
|
||||
// value is active there, and a closed cover's position slider keeps the cover's colour so it never looks
|
||||
// disabled (hui-cover-position-card-feature.ts).
|
||||
bool slider_active() const {
|
||||
if (!available()) return false;
|
||||
auto d = domain();
|
||||
if (d == "media_player") return state != "off" && state != "standby";
|
||||
if (d == "cover" || d == "number" || d == "input_number") return true;
|
||||
return active();
|
||||
return domain() == "cover" || active();
|
||||
}
|
||||
};
|
||||
// Slot position of a tile within the fixed two-column, three-row pages.
|
||||
|
||||
@@ -98,7 +98,6 @@ inline lv_image_dsc_t *cover_ready(const std::string &entity, int size, uint32_t
|
||||
inline lv_obj_t *media_detail_picture = nullptr;
|
||||
constexpr unsigned MEDIA_PICTURE = 14;
|
||||
inline void media_action(Tile &t, int cmd);
|
||||
inline uint32_t domain_accent(const Tile &t);
|
||||
inline const char *icon_for(const Tile &tile);
|
||||
inline void label(lv_obj_t *obj, const std::string &text);
|
||||
inline int active_index = -1;
|
||||
@@ -1778,7 +1777,7 @@ inline void render_history_detail(const Tile &t,bool large,int width,int height,
|
||||
history_request(t.entity,history_hours);
|
||||
c.ready=history_fits(t);
|
||||
const bool line=c.ready?h.line:history_line_guess(t);
|
||||
c.line=line;c.accent=domain_accent(t);
|
||||
c.line=line;c.accent=tile_controls::accent(t);
|
||||
const lv_font_t *big=watch_value_font?watch_value_font:(watch_font?watch_font:detail_font);
|
||||
const lv_font_t *text=control_font?control_font:detail_font;
|
||||
const lv_font_t *small=small_font?small_font:detail_font;
|
||||
@@ -2385,34 +2384,6 @@ inline void pad_vertical(lv_obj_t *obj, int value) {
|
||||
if (lv_obj_get_style_pad_bottom(obj, LV_PART_MAIN) != value) lv_obj_set_style_pad_bottom(obj, value, 0);
|
||||
}
|
||||
inline void set_line_width(lv_obj_t *obj, int value) { if (lv_obj_get_style_line_width(obj, LV_PART_MAIN) != value) lv_obj_set_style_line_width(obj, value, 0); }
|
||||
// HA's default domain/state palette. Pastel circles also identify inactive domains.
|
||||
// Custom Lovelace card/theme CSS is not an entity attribute and is not imported.
|
||||
inline uint32_t domain_accent(const Tile &t) {
|
||||
using namespace theme::ha;
|
||||
auto d=t.domain();
|
||||
if(d=="light" || d=="switch" || d=="input_boolean" || d=="binary_sensor")return AMBER;
|
||||
// A climate card that is off or in an unknown mode keeps HA's orange climate circle.
|
||||
if(d=="climate"){uint32_t c=tile_controls::mode_color(t.state);return c==GREY?ORANGE:c;}
|
||||
if(d=="vacuum")return t.state=="error"?RED:TEAL;
|
||||
if(d=="fan")return CYAN;
|
||||
if(d=="cover")return PURPLE;
|
||||
if(d=="media_player")return LIGHT_BLUE;
|
||||
if(d=="scene" || d=="script")return PURPLE;
|
||||
if(d=="select" || d=="input_select")return INDIGO;
|
||||
if(d=="number" || d=="input_number")return TEAL;
|
||||
if(d=="weather")return t.state=="sunny"?AMBER:t.state=="clear-night"?DEEP_PURPLE:LIGHT_BLUE;
|
||||
if(d=="sun")return t.state=="above_horizon"?ORANGE:DEEP_PURPLE;
|
||||
if(d=="timer")return t.state=="active"?TEAL:t.state=="paused"?ORANGE:GREY;
|
||||
if(d=="person")return t.state=="home"?GREEN:GREY;
|
||||
if(d=="screen")return BLUE;
|
||||
if(d=="sensor"){
|
||||
if(t.unit=="lx")return AMBER;
|
||||
if(t.unit=="°C" || t.unit=="°F")return DEEP_ORANGE;
|
||||
if(t.unit=="kWh" || t.unit=="Wh")return PURPLE;
|
||||
if(t.unit=="%")return TEAL;
|
||||
}
|
||||
return BLUE;
|
||||
}
|
||||
// Custom cards draw into a transparent `extra` container; parts are rebuilt only
|
||||
// when a slot changes mode, so paging keeps RAM use flat on the CYD.
|
||||
// A slot whose next card draws no custom part only hides them: paging back to the clock or
|
||||
@@ -3123,6 +3094,9 @@ inline void render_slot(size_t slot) {
|
||||
else if (t.refused_at && esphome::millis() - t.refused_at < 4000) value = "Refused";
|
||||
else if (d == "light" && t.state == "on" && tile_controls::effect_running(t.extra().effect)) value = t.extra().effect;
|
||||
else if (d == "light" && t.state == "on" && std::isfinite(t.brightness)) value = std::to_string(static_cast<int>(std::lround(std::clamp(t.brightness, 0.0f, 255.0f) * 100 / 255))) + " %";
|
||||
// An airco that is off says so, with the room's temperature when it knows it, as Home Assistant's tile does
|
||||
// (firmware 0.2.71+); while it runs, the tile shows the temperature it is set to.
|
||||
else if (d == "climate" && t.state == "off") { value = tile_controls::climate_mode_text(t.state); if (std::isfinite(t.current)) { char b[32]; snprintf(b, sizeof(b), " · %.1f°", t.current); value += b; } }
|
||||
else if (d == "climate" && std::isfinite(t.target)) { char b[32]; snprintf(b, sizeof(b), "%.1f°", t.target); value = b; }
|
||||
else if (d == "person") value = t.state=="home"?"Home":t.state=="not_home"?"Away":t.state;
|
||||
else if (d == "sun") value = !t.extra().sunrise.empty() && !t.extra().sunset.empty() ? t.extra().sunrise+" - "+t.extra().sunset : t.state=="above_horizon"?"Above the horizon":"Below the horizon";
|
||||
@@ -3245,30 +3219,33 @@ inline void render_slot(size_t slot) {
|
||||
else hide_extra(w);
|
||||
lap(swipe_profile::CUSTOM);
|
||||
}
|
||||
bool on = fresh() && t.active();
|
||||
// Sliders colour more states than the card itself does, such as a playing media player (Tile::slider_active).
|
||||
// Home Assistant's rule (Tile::active): what it calls inactive is grey, such as an airco that is off, a closed
|
||||
// blind, a docked robot or a player in standby (firmware 0.2.71+). A built-in card has no state and keeps its colour.
|
||||
bool on = t.builtin() || (fresh() && t.active());
|
||||
// A closed blind's slider keeps the blind's colour while its card is grey, as in Home Assistant (Tile::slider_active).
|
||||
bool slider_on = fresh() && t.slider_active();
|
||||
bool available=fresh() && t.available();
|
||||
int palette_state=(available?2:0)|(on?1:0)|(slider_on?4:0);
|
||||
if (w.cached_active == palette_state && !w.panel_dirty) { lap(swipe_profile::GEOMETRY); return; }
|
||||
w.cached_active = palette_state;w.panel_dirty=false;
|
||||
uint32_t accent = domain_accent(t);
|
||||
// Off is grey, as in Home Assistant, so a light or a door sensor shows its state at a glance.
|
||||
uint32_t state_color=(t.is_switch()||d=="light"||d=="binary_sensor"||d=="person"||d=="timer") && !on ? theme::STATE_OFF : accent;
|
||||
// Home Assistant's colour for the state (tile_controls::accent), and a lamp's own colour while it is on.
|
||||
uint32_t accent=tile_controls::accent(t);
|
||||
if(d=="light" && on && t.has_hs_color)
|
||||
state_color=lv_color_to_u32(lv_color_hsv_to_rgb(t.hue%360,t.saturation,100))&0xFFFFFF;
|
||||
accent=lv_color_to_u32(lv_color_hsv_to_rgb(t.hue%360,t.saturation,100))&0xFFFFFF;
|
||||
uint32_t state_color=on?accent:theme::STATE_OFF;
|
||||
auto color=lv_color_hex(theme::state(state_color));
|
||||
auto circle_color=lv_color_hex(available?theme::tint(state_color,38):theme::hex(theme::TRACK));
|
||||
// Very pale bulbs still need a visible icon: a touch darker on a light card, a touch lighter on a dark one.
|
||||
auto icon_color=lv_color_hex(available?theme::icon(state_color):theme::hex(theme::OFF));
|
||||
// Off: a grey track without fill or handle, as in Home Assistant.
|
||||
const uint32_t fill=slider_on?state_color:theme::STATE_OFF;
|
||||
const uint32_t fill=slider_on?accent:theme::STATE_OFF;
|
||||
auto fill_color=lv_color_hex(theme::state(fill));
|
||||
set_color(w.slider,LV_STYLE_BG_COLOR,fill_color,LV_PART_INDICATOR);
|
||||
set_color(w.slider,LV_STYLE_BG_COLOR,lv_color_hex(theme::tint(fill,51)),LV_PART_MAIN);
|
||||
slider_bar(w.slider,slider_bar_shown(t,slider_on));
|
||||
// A full-page card lights up in its state colour while on (firmware 0.2.62+): an amber lamp, a purple scene.
|
||||
bool lit=w.full && on && available && !t.transparent;
|
||||
// A full-page card lights up in its state colour while it is on (firmware 0.2.62+): an amber lamp, an open blind, a
|
||||
// playing speaker, never a sensor or the weather (Tile::lights_up, firmware 0.2.71+).
|
||||
bool lit=w.full && on && t.lights_up() && available && !t.transparent;
|
||||
set_color(w.tile,LV_STYLE_BG_COLOR,lv_color_hex(lit?theme::tint(state_color,51):theme::surface(t.background)));
|
||||
set_number(w.tile,LV_STYLE_BORDER_WIDTH,1);
|
||||
// "Background: none" hides only the card; geometry and padding stay identical,
|
||||
@@ -3283,7 +3260,9 @@ inline void render_slot(size_t slot) {
|
||||
set_color(w.unit,LV_STYLE_TEXT_COLOR,theme::color(t.is_page()?theme::CHEVRON:theme::SLATE));
|
||||
set_color(w.title,LV_STYLE_TEXT_COLOR,title_color);
|
||||
set_color(w.value,LV_STYLE_TEXT_COLOR,value_color);
|
||||
style_panel(w,t,color,title_color);
|
||||
// The controls take the state's colour even while the card is grey: a closed blind's position slider stays coloured
|
||||
// (Tile::slider_active), while the slider of something off turns grey and the Off mode key has a grey of its own.
|
||||
style_panel(w,t,lv_color_hex(theme::state(accent)),title_color);
|
||||
// Custom parts follow the card palette: text like the title, lines/dots in the accent.
|
||||
// The sun path sets its own colours on every render, the sunlit area under its arc too: taking the
|
||||
// accent here made that area orange after a palette change and yellow again after the next minute.
|
||||
|
||||
@@ -201,19 +201,22 @@ inline uint32_t outline(uint32_t own) {
|
||||
// What a state looks like in Home Assistant's frontend (its --state-*-color and --*-color variables), the same in both
|
||||
// looks: an amber light is amber at night too. They are data, drawn through state(), foreground(), tint() and icon().
|
||||
namespace ha {
|
||||
constexpr uint32_t AMBER = 0xFFC107; // a light, switch or binary sensor that is on; lux
|
||||
constexpr uint32_t ORANGE = 0xFF9800; // the sun above the horizon, a paused timer or robot, drying
|
||||
constexpr uint32_t AMBER = 0xFFC107; // on: a light, switch, running script or timer, streaming camera; the sun; lux
|
||||
constexpr uint32_t ORANGE = 0xFF9800; // a paused robot, drying, a battery half full, the sun path by day
|
||||
constexpr uint32_t DEEP_ORANGE = 0xFF6F22; // temperature, heating
|
||||
constexpr uint32_t RED = 0xF44336; // a robot in trouble
|
||||
constexpr uint32_t PURPLE = 0x926BC7; // covers, scenes and scripts, energy
|
||||
constexpr uint32_t DEEP_PURPLE = 0x6E41AB; // a clear night, the sun below the horizon
|
||||
constexpr uint32_t INDIGO = 0x3F51B5; // selects
|
||||
constexpr uint32_t BLUE = 0x2196F3; // cooling, a robot on its way back, the settings card, everything else
|
||||
constexpr uint32_t LIGHT_BLUE = 0x03A9F4; // media players, the weather
|
||||
constexpr uint32_t CYAN = 0x00BCD4; // fans
|
||||
constexpr uint32_t TEAL = 0x009688; // numbers, percentages, a running timer, a robot at work
|
||||
constexpr uint32_t GREEN = 0x4CAF50; // someone at home, auto mode
|
||||
constexpr uint32_t GREY = 0x9E9E9E; // anything off or away
|
||||
constexpr uint32_t RED = 0xF44336; // an alarm going off, a robot in trouble, an empty battery
|
||||
constexpr uint32_t PURPLE = 0x926BC7; // covers, scenes, energy
|
||||
constexpr uint32_t DEEP_PURPLE = 0x6E41AB; // a clear night
|
||||
constexpr uint32_t INDIGO = 0x3F51B5; // selects, the sun below the horizon, pouring rain
|
||||
constexpr uint32_t BLUE = 0x2196F3; // cooling, rain, a robot on its way back, someone in a zone, everything else
|
||||
constexpr uint32_t LIGHT_BLUE = 0x03A9F4; // media players, sleet
|
||||
constexpr uint32_t CYAN = 0x00BCD4; // fans, hail
|
||||
constexpr uint32_t TEAL = 0x009688; // numbers, percentages, a robot at work
|
||||
constexpr uint32_t GREEN = 0x4CAF50; // someone at home, auto mode, wind, a full battery
|
||||
constexpr uint32_t GREY = 0x9E9E9E; // anything off or away, fog
|
||||
// The rest of the weather, as Home Assistant colours a condition (--state-weather-*-color): clouds, a partly
|
||||
// cloudy sky, lightning, lightning with rain, and snow, which Home Assistant writes as a value of its own.
|
||||
constexpr uint32_t LIGHT_GREY = 0xBDBDBD, BLUE_GREY = 0x607D8B, YELLOW = 0xFFEB3B, LIME = 0xCDDC39, ICE = 0xC0E0FF;
|
||||
constexpr uint32_t SKY = 0x00A6ED; // a robot that is docked or idle
|
||||
constexpr uint32_t SWITCH_ON = 0xFFB900; // the switch on a history card while on
|
||||
constexpr uint32_t PURPLE_PRESSED = 0x7552A8; // a chosen cover key under a finger
|
||||
|
||||
@@ -59,6 +59,69 @@ inline uint32_t mode_color(const std::string &mode) {
|
||||
if (mode == "dry") return ORANGE;
|
||||
return GREY;
|
||||
}
|
||||
// Binary sensor classes Home Assistant draws red while they are on (its --state-binary_sensor-<class>-on-color): an
|
||||
// alarm, a problem, a low battery, an unlocked lock. header_bar.ALARM_CLASSES gives the top bar the same list.
|
||||
inline bool alarm_class(const std::string &device_class) {
|
||||
for (const char *alarm : {"battery", "carbon_monoxide", "gas", "heat", "lock", "moisture", "problem", "safety", "smoke",
|
||||
"sound", "tamper"})
|
||||
if (device_class == alarm) return true;
|
||||
return false;
|
||||
}
|
||||
// Home Assistant's colour for a weather condition (--state-weather-<condition>-color); a condition it has none for
|
||||
// takes its --state-active-color.
|
||||
inline uint32_t weather_color(const std::string &condition) {
|
||||
using namespace theme::ha;
|
||||
if (condition == "sunny") return AMBER;
|
||||
if (condition == "clear-night") return DEEP_PURPLE;
|
||||
if (condition == "partlycloudy") return BLUE_GREY;
|
||||
if (condition == "cloudy") return LIGHT_GREY;
|
||||
if (condition == "fog") return GREY;
|
||||
if (condition == "rainy") return BLUE;
|
||||
if (condition == "pouring") return INDIGO;
|
||||
if (condition == "snowy") return ICE;
|
||||
if (condition == "snowy-rainy") return LIGHT_BLUE;
|
||||
if (condition == "hail") return CYAN;
|
||||
if (condition == "lightning") return YELLOW;
|
||||
if (condition == "lightning-rainy") return LIME;
|
||||
if (condition == "windy" || condition == "windy-variant") return GREEN;
|
||||
if (condition == "exceptional") return RED;
|
||||
return AMBER;
|
||||
}
|
||||
// The colour of a tile while Home Assistant calls it active (Tile::active; anything inactive is grey): its
|
||||
// stateColorCss() (frontend src/common/entity/state_color.ts) for the domains it colours by state, and a colour of
|
||||
// our own per kind for those it draws in one neutral blue, such as scenes, selects, numbers and sensors (0.2.3).
|
||||
// A state Home Assistant has no colour of its own for takes its --state-active-color, amber (firmware 0.2.71+).
|
||||
// Colours set in a Lovelace card or theme are no entity attributes and never reach the screen.
|
||||
inline uint32_t accent(const Tile &t) {
|
||||
using namespace theme::ha;
|
||||
const auto d = t.domain();
|
||||
if (d == "binary_sensor") return alarm_class(t.device_class) ? RED : AMBER;
|
||||
if (d == "light" || d == "switch" || d == "input_boolean" || d == "script" || d == "timer" || d == "camera") return AMBER;
|
||||
if (d == "climate") { const uint32_t c = mode_color(t.state); return c == GREY ? AMBER : c; }
|
||||
if (d == "vacuum") return t.state == "error" ? RED : TEAL;
|
||||
if (d == "fan") return CYAN;
|
||||
if (d == "cover" || d == "scene") return PURPLE;
|
||||
if (d == "media_player") return LIGHT_BLUE;
|
||||
if (d == "select" || d == "input_select") return INDIGO;
|
||||
if (d == "number" || d == "input_number") return TEAL;
|
||||
if (d == "weather") return weather_color(t.state);
|
||||
if (d == "sun") return t.state == "above_horizon" ? AMBER : INDIGO;
|
||||
// At home green; in another zone blue (--state-person-active-color).
|
||||
if (d == "person") return t.state == "home" ? GREEN : BLUE;
|
||||
if (d == "sensor") {
|
||||
// A battery by its charge, as Home Assistant's battery_color.ts: green from 70 %, orange from 30 %, red below.
|
||||
if (t.device_class == "battery") {
|
||||
char *end = nullptr;
|
||||
const float charge = std::strtof(t.state.c_str(), &end);
|
||||
if (end != t.state.c_str() && *end == '\0' && std::isfinite(charge)) return charge >= 70 ? GREEN : charge >= 30 ? ORANGE : RED;
|
||||
}
|
||||
if (t.unit == "lx") return AMBER;
|
||||
if (t.unit == "°C" || t.unit == "°F") return DEEP_ORANGE;
|
||||
if (t.unit == "kWh" || t.unit == "Wh") return PURPLE;
|
||||
if (t.unit == "%") return TEAL;
|
||||
}
|
||||
return BLUE;
|
||||
}
|
||||
inline const char *mode_icon(const std::string &mode) {
|
||||
if (mode == "off") return glyph::POWER;
|
||||
if (mode == "heat") return glyph::FIRE;
|
||||
|
||||
Reference in New Issue
Block a user