Notation like Home Assistant, each screen in its own language, and the language in every build

From the end-to-end test on the bench boards and an independent review:
- Units spaced as Home Assistant does (54% / 54 % in German and French, 18°, 21.5 °C), in the firmware and the app.
- Grouping from five digits where the language does (1234 but 12.345 in Spanish, Polish, Portuguese): the layout
  message carries group_min and percent_space; an unknown number style no longer refuses a layout.
- Automatic follows Home Assistant's own language for the clock and numbers, also without a translation (Swedish:
  24 hours, 1 234,5; pt-BR: 1.234,5): regions.json from the CLDR for all 63 languages Home Assistant offers.
- 12-hour clock: the top bar says 3:48 PM, the clock card 3:48 (not 03:48), Last runs follow the clock.
- The climate setpoint font has the decimal comma; the value fonts have ², ³ and µ (m³ was a box).
- Weather days: at most three letters without the dot (mié, sáb, czw); the screen took three bytes.
- Texts fall back per key through the base language (pt-BR -> pt -> en) in the firmware and the app.
- Each screen gets the app's words in the language its firmware speaks; firmware from before the languages gets
  English and its own letters, not boxes.
- set_language re-reads what it writes (indentation, comments, quoted keys, CRLF, !include) and every build writes
  the language first; the 24-hour pin no longer hits a fresh install and waits for the old clock switches.
- Tests: region and notation, plural parity with the C++ rules, set_language edge cases, the pin, PUT /api/language.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-19 16:52:34 +02:00
parent ad8a8ccf56
commit 125346d94e
37 changed files with 1419 additions and 448 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ inline std::string number_text(float value) {
if (text.back() == '.') text.pop_back();
return text;
}
inline std::string percent_text(int percent) { return std::to_string(percent) + " %"; }
inline std::string percent_text(int percent) { return screen_text::percent(percent); }
// The roller takes its names as one string, a line per name.
inline std::string joined(const std::vector<std::string> &names) {
std::string out;
+2 -5
View File
@@ -65,8 +65,7 @@ inline std::string duration(uint32_t seconds) {
}
// A value as Home Assistant writes it: fixed decimals, the language's separator between thousands and its decimal
// mark (screen.number: "1,234.5" in English, "1.234,5" in Dutch), "%" and "°" on the number and any other unit after
// a space.
// mark (screen.number: "1,234.5" in English, "1.234,5" in Dutch), the unit spaced as Home Assistant spaces it.
inline std::string number(float value, int decimals, const std::string &unit) {
if (!std::isfinite(value)) return "--";
char b[40];
@@ -77,9 +76,7 @@ inline std::string number(float value, int decimals, const std::string &unit) {
std::string whole = text.substr(0, dot), rest = dot == std::string::npos ? "" : text.substr(dot);
bool zero = whole.find_first_not_of('0') == std::string::npos && rest.find_first_not_of(".0") == std::string::npos;
text = (zero ? "" : sign) + screen_text::write_number(whole, rest.empty() ? std::string() : rest.substr(1));
if (unit.empty()) return text;
if (unit == "%" || unit == "°") return text + unit;
return text + " " + unit;
return screen_text::with_unit(text, unit);
}
// Where part `i` of 24 is drawn and read: the middle of its stretch of the range, as a fraction of the width.
+1 -1
View File
@@ -95,7 +95,7 @@ inline void preview(Row &row) {
lv_obj_set_style_bg_opa(row.slider, row.off ? LV_OPA_TRANSP : LV_OPA_COVER, LV_PART_KNOB);
lv_obj_set_style_bg_color(row.slider, theme::color(row.off ? theme::TRACK : theme::AMBER_TRACK), LV_PART_MAIN);
if (row.off) lv_label_set_text(row.value, screen_text::tr(screen_text::txt::ha_off));
else lv_label_set_text_fmt(row.value, "%d %%", value);
else lv_label_set_text(row.value, screen_text::percent(value).c_str());
}
}
inline void event(lv_event_t *e) {
+50 -30
View File
@@ -44,8 +44,17 @@ inline esphome::ESPPreferenceObject buttons_preference;
inline esphome::ESPPreferenceObject swipe_preference;
inline esphome::ESPPreferenceObject home_preference;
inline esphome::ESPPreferenceObject dark_preference;
// The number format of Settings -> Language & region (app 0.2.90), kept for the next start: screen_text::number_style.
// The number format of Settings -> Language & region (app 0.2.90), kept for the next start: screen_text::number_style
// (bits 0-3), number_group_min (4-5) and number_percent (6-7) in one word.
inline esphome::ESPPreferenceObject numbers_preference;
inline uint32_t screen_text_numbers() {
return screen_text::number_style | (uint32_t) screen_text::number_group_min << 4 | (uint32_t) screen_text::number_percent << 6;
}
inline void screen_text_numbers(uint32_t word) {
screen_text::number_style = (word & 0xF) <= 3 ? word & 0xF : 0;
screen_text::number_group_min = (word >> 4 & 3) <= 2 ? word >> 4 & 3 : 0;
screen_text::number_percent = (word >> 6 & 3) <= 2 ? word >> 6 & 3 : 0;
}
inline Model model;
inline std::string inbox;
inline const lv_font_t *watch_font = nullptr;
@@ -91,6 +100,11 @@ inline const char *weather_icon(const std::string &condition);
inline const char *weather_text(const std::string &condition);
inline std::string timer_text(const Tile &t);
inline std::string last_run_text(uint32_t epoch);
// "07:12": a time of day as Home Assistant and ESP Screens send it, for screen_text::clock_text.
inline std::string hhmm(const esphome::ESPTime &time) {
char b[8]; snprintf(b, sizeof(b), "%02d:%02d", time.hour, time.minute);
return b;
}
inline void history_received();
// Camera images full screen and on an alert (firmware 0.2.57+, the Guition binds them; see the end of this file).
inline void camera_open(const std::string &entity, const std::string &name);
@@ -153,7 +167,7 @@ inline void load_settings() {
buttons_preference = esphome::global_preferences->make_preference<uint32_t>(0x50474231);
numbers_preference = esphome::global_preferences->make_preference<uint32_t>(0x4E554D31);
uint32_t numbers_saved=0;
if(numbers_preference.load(&numbers_saved) && numbers_saved<=3)screen_text::number_style=(uint8_t)numbers_saved;
if(numbers_preference.load(&numbers_saved))screen_text_numbers(numbers_saved);
uint32_t swipe_saved=0;
if(swipe_preference.load(&swipe_saved))swipe_pages=swipe_saved==1;
uint32_t dark_saved=0;
@@ -308,8 +322,6 @@ inline std::string receive(const std::string &payload) {
root["rotation"].as<unsigned>()>270 || root["rotation"].as<unsigned>()%90!=0))return false;
// The clock and the number format of Settings -> Language & region (app 0.2.90), the same for every screen.
if(!root["clock_24h"].isNull() && !root["clock_24h"].is<bool>())return false;
if(!root["numbers"].isNull() && (!root["numbers"].is<const char*>() ||
screen_text::number_style_of(root["numbers"].as<std::string>())<0))return false;
if(!root["keepalive"].isNull() && (!root["keepalive"].is<unsigned>() ||
root["keepalive"].as<unsigned>()<5 || root["keepalive"].as<unsigned>()>3600))return false;
// Explicit grid positions (0.2.26+), one absolute slot per entity; absent on older managers.
@@ -345,11 +357,16 @@ inline std::string receive(const std::string &payload) {
}
bool rotation_changed=false;
if(root["clock_24h"].is<bool>())settings.clock_24h=root["clock_24h"].as<bool>()?1:0;
// How numbers are written; a value this firmware doesn't know is left out, never the layout (a newer app may
// know more styles).
bool format_changed=false;
if(root["numbers"].is<const char*>()){
int style=screen_text::number_style_of(root["numbers"].as<std::string>());
if(style!=screen_text::number_style){screen_text::number_style=(uint8_t)style;uint32_t saved=(uint32_t)style;numbers_preference.save(&saved);format_changed=true;}
}
int style=root["numbers"].is<const char*>()?screen_text::number_style_of(root["numbers"].as<std::string>()):-1;
uint32_t numbers=screen_text_numbers();
if(style>=0)numbers=(numbers&~0xFu)|(uint32_t)style;
if(root["group_min"].is<unsigned>() && root["group_min"].as<unsigned>()>=1 && root["group_min"].as<unsigned>()<=2)
numbers=(numbers&~0x30u)|(root["group_min"].as<unsigned>()<<4);
if(root["percent_space"].is<bool>())numbers=(numbers&~0xC0u)|((root["percent_space"].as<bool>()?2u:1u)<<6);
if(numbers!=screen_text_numbers()){screen_text_numbers(numbers);numbers_preference.save(&numbers);format_changed=true;}
if(rotation_supported && root["rotation"].is<unsigned>() && rotation!=root["rotation"].as<unsigned>()){
rotation=root["rotation"].as<unsigned>();rotation_preference.save(&rotation);rotation_changed=true;
}
@@ -591,7 +608,7 @@ inline std::string receive(const std::string &payload) {
if (extra["days"].is<JsonArray>()) for (JsonVariant day : extra["days"].as<JsonArray>()) {
if (next.forecast.size() == 5) break;
next.forecast.emplace_back(); auto &f = next.forecast.back();
f.day = string(day["d"], 3); f.condition = string(day["c"], 20); f.high = number(day["h"]); f.low = number(day["l"]);
f.day = string(day["d"], 8); f.condition = string(day["c"], 20); f.high = number(day["h"]); f.low = number(day["l"]);
f.rain = number(day["p"]); f.mm = number(day["r"]);
}
if (extra["hours"].is<JsonArray>()) for (JsonVariant hour : extra["hours"].as<JsonArray>()) {
@@ -1086,10 +1103,9 @@ inline lv_obj_t *detail_text(lv_obj_t *parent,const std::string &text,int x,int
}
// "30%", "30% · 1.7 mm" or "1.7 mm": whatever the provider reports; empty when dry.
inline std::string rain_text(float chance,float mm,bool with_mm){
char b[32];
if(std::isfinite(chance) && chance>=0){
if(with_mm && std::isfinite(mm) && mm>=0.05f){snprintf(b,sizeof(b),"%d%% · ",(int)std::lround(chance));return b+screen_text::decimal(mm,1)+" mm";}
snprintf(b,sizeof(b),"%d%%",(int)std::lround(chance));return b;
if(with_mm && std::isfinite(mm) && mm>=0.05f)return screen_text::percent((int)std::lround(chance))+" · "+screen_text::with_unit(screen_text::decimal(mm,1),"mm");
return screen_text::percent((int)std::lround(chance));
}
if(std::isfinite(mm) && mm>=0.05f)return screen_text::decimal(mm,mm<10?1:0)+" mm";
return "";
@@ -1134,7 +1150,7 @@ inline void render_weather_detail(const Tile &t,bool large,int width,int height,
detail_text(now,t.available()?weather_text(t.state):tr(txt::ha_unavailable),text_x,cy+(hero-lines_h)/2,text_w,detail_font,LV_TEXT_ALIGN_LEFT,ink);
std::string details;
if(std::isfinite(weather.feels))details=fill(txt::weather_feels_like,"n",(int)std::lround(weather.feels));
if(std::isfinite(t.humidity)){snprintf(b,sizeof(b),"%d%%",(int)std::lround(t.humidity));details+=(details.empty()?"":" · ")+std::string(b);}
if(std::isfinite(t.humidity))details+=(details.empty()?"":" · ")+screen_text::percent((int)std::lround(t.humidity));
if(std::isfinite(weather.wind)){snprintf(b,sizeof(b),"%.0f %s",weather.wind,weather.wind_unit.empty()?"km/h":weather.wind_unit.c_str());details+=(details.empty()?"":" · ")+std::string(b);}
detail_text(now,details,text_x,cy+(hero-lines_h)/2+text_h+(large?2:0),text_w,small,LV_TEXT_ALIGN_LEFT,muted);
// Next hours inside the same card: time, icon, temperature, rain per column.
@@ -1221,7 +1237,7 @@ inline void vacuum_battery(lv_obj_t *parent,int x,int y,int w,int h,float level)
// Without a parent it only measures.
inline int vacuum_power(lv_obj_t *parent,const Tile &t,int x,int y,const lv_font_t *font,bool large){
const lv_font_t *bolt_font=large && watch_icon_font?watch_icon_font:mini_icon_font;
std::string percent=std::to_string((int)std::lround(t.battery))+"%";
std::string percent=screen_text::percent((int)std::lround(t.battery));
int h=lv_font_get_line_height(font),meter_w=large?30:22,meter_h=large?15:11,gap=large?10:6,words=text_width(percent,font);
int bolt_w=t.extra().charging && bolt_font?text_width("\U000F0241",bolt_font):0;
int width=meter_w+3+gap+words+(bolt_w?gap/2+bolt_w:0);
@@ -1382,7 +1398,7 @@ inline void cover_slider_event(lv_event_t *e){
int raw=lv_slider_get_value(slider);
if(raw<0||raw>1000){raw=std::clamp(raw,0,1000);lv_slider_set_value(slider,raw,LV_ANIM_OFF);}
int percent=(int)std::lround(raw/10.0f);
if(auto *value=cover_values[tilt?1:0])label(value,std::to_string(tilt?percent:100-percent)+"%");
if(auto *value=cover_values[tilt?1:0])label(value,screen_text::percent(tilt?percent:100-percent));
return;
}
if(code!=LV_EVENT_RELEASED||detail_index>=model.count)return;
@@ -1462,7 +1478,7 @@ inline void cover_battery(const Tile &t,bool large,int width){
int level=(int)std::lround(std::clamp(t.battery,0.0f,100.0f));
int line=lv_font_get_line_height(font),block=meter_h+gap+line,cx=width-bar_x-bar/2,y=bar_y+(bar-block)/2;
vacuum_battery(detail_root,cx-meter_w/2-1,y,meter_w,meter_h,t.battery);
detail_text(detail_root,std::to_string(level)+"%",cx-bar/2-8,y+meter_h+gap,bar+16,font,LV_TEXT_ALIGN_CENTER,theme::MUTED);
detail_text(detail_root,screen_text::percent(level),cx-bar/2-8,y+meter_h+gap,bar+16,font,LV_TEXT_ALIGN_CENTER,theme::MUTED);
}
inline void render_cover_detail(Tile &t,bool large,int width,int height,int pad){
auto card=tile_controls::cover_card(t);
@@ -1485,7 +1501,7 @@ inline void render_cover_detail(Tile &t,bool large,int width,int height,int pad)
if(card.position)parts[n++]={false,t.position,tr(txt::cover_position)};
if(card.tilt)parts[n++]={true,t.extra().tilt,tr(txt::cover_tilt)};
int value_h=lv_font_get_line_height(big),caption_h=lv_font_get_line_height(text);
auto percent=[](float value){return std::isfinite(value)?std::to_string((int)std::lround(std::clamp(value,0.0f,100.0f)))+"%":std::string("--");};
auto percent=[](float value){return std::isfinite(value)?screen_text::percent((int)std::lround(std::clamp(value,0.0f,100.0f))):std::string("--");};
if(large){
// Columns: the slider with its value and name below.
int edge=16,slider_h=box_h-2*edge-value_h-caption_h+4,slider_w=n==2?120:140,column_gap=48;
@@ -1911,7 +1927,7 @@ inline bool media_off(const Tile &t){return t.state=="off" || t.state=="standby"
inline std::string media_volume_text(const Tile &t){
if(t.muted)return tr(txt::media_muted);
if(!std::isfinite(t.volume))return "";
return std::to_string((int)std::lround(std::clamp(t.volume,0.0f,1.0f)*100))+" %";
return screen_text::percent((int)std::lround(std::clamp(t.volume,0.0f,1.0f)*100));
}
// A rounded box without a style of its own: the art's placeholder, the bar's track and its fill.
inline lv_obj_t *media_box(lv_obj_t *parent,lv_obj_t *existing,const media_card::Rect &r,uint32_t color,int radius){
@@ -2072,7 +2088,7 @@ inline void show_detail(unsigned index){
std::string state=d=="cover"?cover_status_line(t):detail_state(t);
// The vacuum and history cards draw their own state.
const bool with_history=history_card(t);
if(d!="vacuum"&&d!="media_player"&&!with_history){detail_status=detail_label(detail_root,state+(t.unit.empty()?"":" "+t.unit),pad,large?80:50,width-2*pad);lv_obj_set_style_text_align(detail_status,LV_TEXT_ALIGN_CENTER,0);lv_obj_set_style_text_color(detail_status,theme::color(theme::MUTED),0);}
if(d!="vacuum"&&d!="media_player"&&!with_history){detail_status=detail_label(detail_root,screen_text::with_unit(state,t.unit),pad,large?80:50,width-2*pad);lv_obj_set_style_text_align(detail_status,LV_TEXT_ALIGN_CENTER,0);lv_obj_set_style_text_color(detail_status,theme::color(theme::MUTED),0);}
if(with_history){
render_history_detail(t,large,width,height,pad);
}else if(d=="vacuum"){
@@ -2178,7 +2194,7 @@ inline std::string last_run_text(uint32_t epoch) {
auto when = esphome::ESPTime::from_epoch_local(epoch);
auto now = now_time ? now_time() : esphome::ESPTime{};
if (!when.is_valid()) return tr(txt::script_never_run);
char clock[8]; snprintf(clock, sizeof(clock), "%02d:%02d", when.hour, when.minute);
std::string clock = screen_text::clock_text(hhmm(when), screen_settings::current.clock_24h != 0);
if (now.is_valid() && now.year == when.year && now.day_of_year == when.day_of_year) return fill(txt::script_last_time, "time", clock);
if (now.is_valid() && now.year == when.year && now.day_of_year == when.day_of_year + 1) return fill(txt::script_yesterday_time, "time", clock);
std::string date = fill(fill(txt::date_day_month, "day", std::to_string(when.day_of_month)), "month", month_short(when));
@@ -2470,8 +2486,12 @@ inline lv_obj_t *part_line(Widgets &w,unsigned i,lv_point_precise_t *points,unsi
if(!p){p=lv_line_create(w.extra);lv_obj_remove_flag(p,LV_OBJ_FLAG_CLICKABLE);lv_obj_set_style_line_rounded(p,true,0);}
set_line_width(p,width);lv_line_set_points(p,points,count);lv_obj_set_pos(p,x,y);return p;
}
// The big digits of the clock card: "07:12" on 24 hours, "7:12" on 12 (the clock font has no letters for AM and PM).
inline std::string time_text(esphome::ESPTime now) {
return now.is_valid() ? now.strftime(screen_settings::current.clock_24h ? "%H:%M" : "%I:%M") : "--:--";
if (!now.is_valid()) return "--:--";
if (screen_settings::current.clock_24h) return hhmm(now);
char b[8]; snprintf(b, sizeof(b), "%d:%02d", now.hour % 12 ? now.hour % 12 : 12, now.minute);
return b;
}
// Digital: big time over the date. Analog: index strokes (numerals at 12/3/6/9 on
// large cards) with hour and minute hands. A single card adds a calendar block
@@ -2794,7 +2814,7 @@ inline int layout_panel(Widgets &w,const Tile &t,bool large,int content_w,int co
panel_w=count?count*m.key_w+(count-1)*m.gap:0;
}else if(mode=="setpoint"||mode=="stepper"){
float shown=std::isfinite(t.edit_value)?t.edit_value:tile_controls::edit_target(t);
std::string suffix=d=="climate"?"°":t.unit.empty()?"":" "+t.unit;
std::string suffix=d=="climate"?"°":screen_text::unit_suffix(t.unit);
label(w.pill_value,tile_controls::format_value(shown,tile_controls::edit_step(t),suffix.c_str()));
panel_w=m.pill_w;panel_h=m.key_h+2;
}else if(tile_controls::is_slider(mode)){
@@ -2881,7 +2901,7 @@ inline void control_event(lv_event_t *e) {
float current=std::isfinite(t.edit_value)?t.edit_value:tile_controls::edit_target(t);
t.edit_value=tile_controls::step_value(current,tile_controls::edit_step(t),t.minimum,t.maximum,command==tile_controls::STEP_UP?1:-1);
t.edit_since=now;t.edit_sent=false;
if(w.pill_value){std::string suffix=t.domain()=="climate"?"°":t.unit.empty()?"":" "+t.unit;label(w.pill_value,tile_controls::format_value(t.edit_value,tile_controls::edit_step(t),suffix.c_str()));}
if(w.pill_value){std::string suffix=t.domain()=="climate"?"°":screen_text::unit_suffix(t.unit);label(w.pill_value,tile_controls::format_value(t.edit_value,tile_controls::edit_step(t),suffix.c_str()));}
return;
}
if(t.waiting(now))return;
@@ -3047,7 +3067,7 @@ inline void render_full(Widgets &w,const Tile &t,bool custom,bool clock,bool sun
lv_obj_set_height(w.title,name_h);
lv_obj_set_pos(w.title,0,top+circle+gap);lv_obj_set_width(w.title,content_w);
lv_obj_set_pos(w.value,0,top+circle+gap+name_h+2);lv_obj_set_width(w.value,content_w);
if(!unit.empty())label(w.value,value+" "+unit);
if(!unit.empty())label(w.value,screen_text::with_unit(value,unit));
return;
}
// The head as on the double-width card: the circle at the left, name and state beside it.
@@ -3092,7 +3112,7 @@ inline void render_full(Widgets &w,const Tile &t,bool custom,bool clock,bool sun
std::string middle;const lv_font_t *mid_font=watch_value_font?watch_value_font:w.value_font;
auto d=t.domain();
if(d=="climate" && std::isfinite(t.current))middle=screen_text::decimal(t.current,1)+"°";
else if(d=="cover" && std::isfinite(t.position)){middle=std::to_string(static_cast<int>(std::lround(t.position)))+" %";}
else if(d=="cover" && std::isfinite(t.position)){middle=screen_text::percent(static_cast<int>(std::lround(t.position)));}
else if(d=="media_player" && !t.extra().media_title.empty()){middle=t.extra().media_title;mid_font=room_label?lv_obj_get_style_text_font(room_label,LV_PART_MAIN):w.title_font;}
int mid_font_h=lv_font_get_line_height(mid_font);
if(middle.empty() || mid_h<mid_font_h)return;
@@ -3120,7 +3140,7 @@ inline void render_slot(size_t slot) {
else if (!fresh() || !t.available()) value = tr(txt::ha_unavailable);
else if (t.refused_at && esphome::millis() - t.refused_at < 4000) value = tr(txt::tile_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))) + " %";
else if (d == "light" && t.state == "on" && std::isfinite(t.brightness)) value = screen_text::percent(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)) value += " · " + screen_text::decimal(t.current, 1) + "°"; }
@@ -3145,8 +3165,8 @@ inline void render_slot(size_t slot) {
else if (!t.unit.empty() && !watch) value = screen_text::localize(value) + " " + t.unit;
else if (!t.unit.empty() || d == "number" || d == "input_number" || d == "counter") value = screen_text::localize(value);
bool pending=t.loading(esphome::millis());
if(d=="weather" && std::isfinite(t.current)) {value=screen_text::decimal(t.current,1);if(!watch)value+=" "+t.unit;}
if(d=="vacuum" && std::isfinite(t.battery))value += " / "+std::to_string((int)t.battery)+"%";
if(d=="weather" && std::isfinite(t.current)) {value=screen_text::decimal(t.current,1);if(!watch)value=screen_text::with_unit(value,t.unit);}
if(d=="vacuum" && std::isfinite(t.battery))value += " / "+screen_text::percent((int)t.battery);
// Direct controls: only a wide card in the standard layout has room for the panel.
bool with_panel=w.wide && !t.controls.empty() && !t.builtin() && !watch && t.inline_control!="slider" && fresh() && t.available();
if(with_panel){std::string status=tile_controls::status_text(t);if(!status.empty())value=status;}
@@ -3488,7 +3508,7 @@ inline void draw_header(bool live) {
p.item = i;
if (item.kind == Kind::analog) { p.dial = true; p.width = dial; }
else {
p.text = item.kind == Kind::clock ? (now.is_valid() ? now.strftime(screen_settings::current.clock_24h ? "%H:%M" : "%I:%M") : std::string("--:--"))
p.text = item.kind == Kind::clock ? (now.is_valid() ? screen_text::clock_text(hhmm(now), screen_settings::current.clock_24h != 0) : std::string("--:--"))
: item.kind == Kind::date ? (now.is_valid() ? header_bar::date_text(now.day_of_week, now.day_of_month, now.month) : std::string("—"))
: item.kind == Kind::ago ? header_bar::ago_text(item.epoch, now_epoch()) : item.text;
lv_font_glyph_dsc_t g;
@@ -3886,7 +3906,7 @@ inline void tick() {
}
for(unsigned i=0;i<detail_action_count;++i){if(waiting||!fresh()||!t.available())lv_obj_add_state(detail_actions[i],LV_STATE_DISABLED);else lv_obj_remove_state(detail_actions[i],LV_STATE_DISABLED);}
std::string status=waiting?std::string(tr(t.confirmed?txt::tile_confirmed:txt::tile_command_sent)):t.domain()=="cover"?cover_status_line(t):detail_state(t);
if(detail_status)label(detail_status,status+(!waiting && !t.unit.empty()?" "+t.unit:""));
if(detail_status)label(detail_status,waiting?status:screen_text::with_unit(status,t.unit));
// The vacuum card lays its state out with the room and battery beside it, so a new text draws the
// card again; once Home Assistant answered, the new state says enough.
if(detail_badge_status && t.domain()=="vacuum"){
+24 -5
View File
@@ -47,9 +47,12 @@ inline std::string plural(uint16_t id, int n) {
return fill(form, "n", std::to_string(n));
}
// How numbers are written (app 0.2.90): ESP Screens sends the choice of Settings -> Language & region, the one place
// for every screen. 0 follows the language (screen.number), 1 is "1,234.5", 2 "1.234,5" and 3 "1 234,5".
inline uint8_t number_style = 0;
// How numbers are written (app 0.2.90): ESP Screens sends the choice of Settings -> Language & region with every layout,
// the one place for every screen, worked out for the language Home Assistant speaks. Until a screen has it (and with an
// app that doesn't send it), the screen writes numbers the way its own language does (screen.number); 0 means that.
inline uint8_t number_style = 0; // 1 "1,234.5", 2 "1.234,5", 3 "1 234,5"
inline uint8_t number_group_min = 0; // CLDR's minimum grouping digits: 2 writes 1234 but 12.345 (Spanish, Polish)
inline uint8_t number_percent = 0; // 1 "54%", 2 "54 %" (German, French: Home Assistant's own rule)
inline char decimal_mark() {
if (number_style == 1) return '.';
if (number_style == 2 || number_style == 3) return ',';
@@ -66,8 +69,7 @@ inline const char *group_mark() {
// How many digits a whole number needs before it gets separators: 4 ("1,234") in most languages, 5 in Italian, Spanish
// and Polish, which write 1234 but 12.345 (CLDR's minimum grouping digits, as Home Assistant's own numbers do).
inline size_t group_from() {
if (number_style) return 4;
int minimum = atoi(tr(txt::number_group_min));
int minimum = number_group_min ? number_group_min : atoi(tr(txt::number_group_min));
return minimum >= 2 ? 5 : 4;
}
// A number the way this screen writes numbers: `whole` digits (no sign) and the digits after the point.
@@ -93,6 +95,23 @@ inline std::string localize(const std::string &state) {
std::string fraction = dot == std::string::npos ? std::string() : state.substr(dot + 1);
return state.substr(0, start) + write_number(whole, fraction);
}
// What follows a number for a percentage: "%" or " %".
inline const char *percent_sign() {
if (number_percent == 1) return "%";
if (number_percent == 2) return " %";
return tr(txt::number_percent);
}
// What follows a number for its unit, spaced as Home Assistant spaces them: " °C", "%" (" %" in German and French),
// "°" (Home Assistant's blankBeforeUnit).
inline std::string unit_suffix(const std::string &unit) {
if (unit.empty() || unit == "\u00B0") return unit;
if (unit == "%") return percent_sign();
return " " + unit;
}
// A value with its unit: "21.5 °C", "54%", "18°".
inline std::string with_unit(const std::string &value, const std::string &unit) { return value + unit_suffix(unit); }
// A whole percentage: "54%", "54 %" in German and French.
inline std::string percent(int value) { return std::to_string(value) + percent_sign(); }
// A time "HH:MM" as Home Assistant or ESP Screens sends it, written for the screen's clock: "07:12" on 24 hours,
// "7:12 AM" on 12 (in the language's day periods); `compact` leaves out ":00" ("2 PM"), as under a graph. Anything
// else stays as it is.
+16 -6
View File
@@ -94,19 +94,29 @@ def cpp_string(text):
return '"' + ''.join(out) + '"'
def chain(language, folder=TRANSLATIONS):
"""The files a language's texts come from, most specific first: pt-BR, then pt, then English."""
found = [language]
base = language.split('-')[0]
if base != language and load(base, folder) is not None:
found.append(base)
return found + ([] if 'en' in found else ['en'])
def table(code, folder=TRANSLATIONS):
"""(language, keys, texts, plural rule) of a build: every English key, in its language where it has one."""
"""(language, keys, texts, plural rule) of a build: every English key, in its language where it has one, else in its
base language (a small pt-BR file takes the rest from pt), else in English."""
english = load('en', folder)
language = resolve(code, folder)
own = dict(screen_pairs(load(language, folder))) if language != 'en' else {}
files = [load(name, folder) or {} for name in chain(language, folder)]
texts_of = [dict(screen_pairs(data)) for data in files]
keys, texts = [], []
for key, text in screen_pairs(english):
keys.append(key)
translated = own.get(key)
# Only an empty text is missing: a separator of one space (French thousands) is a text.
texts.append(translated if isinstance(translated, str) and translated != '' else text)
meta = (load(language, folder) or {}).get('_meta', {})
rule = meta.get('plural') if meta.get('plural') in PLURAL_RULES else 'one_other'
texts.append(next((own[key] for own in texts_of if isinstance(own.get(key), str) and own[key] != ''), text))
plural = next((data.get('_meta', {}).get('plural') for data in files if data.get('_meta', {}).get('plural')), None)
rule = plural if plural in PLURAL_RULES else 'one_other'
return language, keys, texts, rule
+265 -264
View File
@@ -9,274 +9,275 @@ namespace txt {
constexpr uint16_t number_decimal = 0;
constexpr uint16_t number_group = 1;
constexpr uint16_t number_group_min = 2;
constexpr uint16_t time_just_now = 3;
constexpr uint16_t time_minutes_ago = 4;
constexpr uint16_t time_hours_ago = 5;
constexpr uint16_t time_yesterday = 6;
constexpr uint16_t time_days_ago = 7;
constexpr uint16_t time_weeks_ago = 8;
constexpr uint16_t time_months_ago = 9;
constexpr uint16_t time_years_ago = 10;
constexpr uint16_t time_in_minutes = 11;
constexpr uint16_t time_in_hours = 12;
constexpr uint16_t time_tomorrow = 13;
constexpr uint16_t time_in_days = 14;
constexpr uint16_t time_am = 15;
constexpr uint16_t time_pm = 16;
constexpr uint16_t date_top_bar = 62;
constexpr uint16_t date_full = 63;
constexpr uint16_t date_day_month = 64;
constexpr uint16_t date_weekday_time = 65;
constexpr uint16_t duration_seconds = 66;
constexpr uint16_t duration_minutes = 67;
constexpr uint16_t duration_hours = 68;
constexpr uint16_t duration_hours_minutes = 69;
constexpr uint16_t duration_days = 70;
constexpr uint16_t duration_days_hours = 71;
constexpr uint16_t status_connecting = 72;
constexpr uint16_t status_waiting = 73;
constexpr uint16_t status_loading_tiles = 74;
constexpr uint16_t status_ha_not_connected = 75;
constexpr uint16_t status_manager_not_active = 76;
constexpr uint16_t status_choose_tiles = 77;
constexpr uint16_t status_home = 78;
constexpr uint16_t status_no_network = 79;
constexpr uint16_t settings_title = 80;
constexpr uint16_t settings_brightness = 81;
constexpr uint16_t settings_night = 82;
constexpr uint16_t settings_screen = 83;
constexpr uint16_t settings_this_screen = 84;
constexpr uint16_t settings_dark_mode = 85;
constexpr uint16_t settings_auto_standby = 86;
constexpr uint16_t settings_standby_after = 87;
constexpr uint16_t settings_standby_brightness = 88;
constexpr uint16_t settings_night_mode = 89;
constexpr uint16_t settings_starts = 90;
constexpr uint16_t settings_ends = 91;
constexpr uint16_t settings_night_brightness = 92;
constexpr uint16_t settings_back_to_page_1 = 93;
constexpr uint16_t settings_after = 94;
constexpr uint16_t settings_also_on_standby = 95;
constexpr uint16_t settings_swipe_between_pages = 96;
constexpr uint16_t settings_page_buttons = 97;
constexpr uint16_t settings_rotation = 98;
constexpr uint16_t settings_address = 99;
constexpr uint16_t settings_firmware = 100;
constexpr uint16_t settings_home_assistant = 101;
constexpr uint16_t settings_restart = 102;
constexpr uint16_t settings_connected = 103;
constexpr uint16_t settings_not_connected = 104;
constexpr uint16_t settings_tap_again_to_restart = 105;
constexpr uint16_t settings_seconds = 106;
constexpr uint16_t settings_minutes = 107;
constexpr uint16_t settings_hours = 108;
constexpr uint16_t settings_hours_minutes = 109;
constexpr uint16_t tile_tap_to_open = 110;
constexpr uint16_t tile_page = 111;
constexpr uint16_t tile_refused = 112;
constexpr uint16_t tile_command_sent = 113;
constexpr uint16_t tile_confirmed = 114;
constexpr uint16_t alert_notification = 115;
constexpr uint16_t alert_ok = 116;
constexpr uint16_t light_color = 117;
constexpr uint16_t light_color_temperature = 118;
constexpr uint16_t light_brightness = 119;
constexpr uint16_t light_waiting = 120;
constexpr uint16_t effects_effect = 121;
constexpr uint16_t effects_nothing = 122;
constexpr uint16_t effects_loading = 123;
constexpr uint16_t effects_not_connected = 124;
constexpr uint16_t climate_target = 125;
constexpr uint16_t climate_mode = 126;
constexpr uint16_t climate_fan = 127;
constexpr uint16_t climate_swing = 128;
constexpr uint16_t climate_now = 129;
constexpr uint16_t vacuum_check_robot = 130;
constexpr uint16_t vacuum_auto_suction = 131;
constexpr uint16_t vacuum_robot_chooses = 132;
constexpr uint16_t vacuum_per_room = 133;
constexpr uint16_t vacuum_speed_quiet = 134;
constexpr uint16_t vacuum_speed_balanced = 135;
constexpr uint16_t vacuum_speed_turbo = 136;
constexpr uint16_t vacuum_speed_max = 137;
constexpr uint16_t vacuum_pause_cleaning = 138;
constexpr uint16_t vacuum_pause = 139;
constexpr uint16_t vacuum_resume = 140;
constexpr uint16_t vacuum_start_cleaning = 141;
constexpr uint16_t vacuum_clean = 142;
constexpr uint16_t vacuum_dock = 143;
constexpr uint16_t cover_position = 144;
constexpr uint16_t cover_tilt = 145;
constexpr uint16_t cover_tilt_value = 146;
constexpr uint16_t weather_feels_like = 147;
constexpr uint16_t weather_no_forecast = 148;
constexpr uint16_t weather_coming_days = 149;
constexpr uint16_t history_no_data = 150;
constexpr uint16_t history_average = 151;
constexpr uint16_t history_average_of = 152;
constexpr uint16_t history_now = 153;
constexpr uint16_t history_range_hour = 154;
constexpr uint16_t history_range_day = 155;
constexpr uint16_t history_range_week = 156;
constexpr uint16_t history_high = 157;
constexpr uint16_t history_low = 158;
constexpr uint16_t history_times = 159;
constexpr uint16_t history_loading = 160;
constexpr uint16_t history_empty = 161;
constexpr uint16_t history_unavailable = 162;
constexpr uint16_t media_muted = 163;
constexpr uint16_t media_not_playing = 164;
constexpr uint16_t media_loading = 165;
constexpr uint16_t timer_running = 166;
constexpr uint16_t timer_paused = 167;
constexpr uint16_t timer_stopped = 168;
constexpr uint16_t timer_pause = 169;
constexpr uint16_t timer_start = 170;
constexpr uint16_t timer_cancel = 171;
constexpr uint16_t timer_paused_left = 172;
constexpr uint16_t sun_name = 173;
constexpr uint16_t sun_sunrise = 174;
constexpr uint16_t sun_sunset = 175;
constexpr uint16_t sun_rise = 176;
constexpr uint16_t sun_set = 177;
constexpr uint16_t script_running = 178;
constexpr uint16_t script_never_run = 179;
constexpr uint16_t script_last_time = 180;
constexpr uint16_t script_yesterday_time = 181;
constexpr uint16_t script_last_date = 182;
constexpr uint16_t camera_no_image = 183;
constexpr uint16_t camera_loading = 184;
constexpr uint16_t camera_live = 185;
constexpr uint16_t camera_recording = 186;
constexpr uint16_t camera_tap_to_view = 187;
constexpr uint16_t camera_no_image_yet = 188;
constexpr uint16_t calibration_title = 189;
constexpr uint16_t calibration_touch_check = 190;
constexpr uint16_t calibration_hold = 191;
constexpr uint16_t calibration_crosshair = 192;
constexpr uint16_t calibration_gently = 193;
constexpr uint16_t calibration_same = 194;
constexpr uint16_t calibration_rejected = 195;
constexpr uint16_t calibration_save_failed = 196;
constexpr uint16_t ha_on = 197;
constexpr uint16_t ha_off = 198;
constexpr uint16_t ha_unavailable = 199;
constexpr uint16_t ha_climate_off = 200;
constexpr uint16_t ha_climate_heat = 201;
constexpr uint16_t ha_climate_cool = 202;
constexpr uint16_t ha_climate_heat_cool = 203;
constexpr uint16_t ha_climate_auto = 204;
constexpr uint16_t ha_climate_dry = 205;
constexpr uint16_t ha_climate_fan_only = 206;
constexpr uint16_t ha_hvac_action_heating = 207;
constexpr uint16_t ha_hvac_action_cooling = 208;
constexpr uint16_t ha_hvac_action_idle = 209;
constexpr uint16_t ha_hvac_action_off = 210;
constexpr uint16_t ha_hvac_action_drying = 211;
constexpr uint16_t ha_hvac_action_fan = 212;
constexpr uint16_t ha_hvac_action_preheating = 213;
constexpr uint16_t ha_hvac_action_defrosting = 214;
constexpr uint16_t ha_cover_open = 215;
constexpr uint16_t ha_cover_closed = 216;
constexpr uint16_t ha_cover_opening = 217;
constexpr uint16_t ha_cover_closing = 218;
constexpr uint16_t ha_media_playing = 219;
constexpr uint16_t ha_media_paused = 220;
constexpr uint16_t ha_media_idle = 221;
constexpr uint16_t ha_media_standby = 222;
constexpr uint16_t ha_person_home = 223;
constexpr uint16_t ha_person_not_home = 224;
constexpr uint16_t ha_sun_above_horizon = 225;
constexpr uint16_t ha_sun_below_horizon = 226;
constexpr uint16_t ha_vacuum_docked = 227;
constexpr uint16_t ha_vacuum_cleaning = 228;
constexpr uint16_t ha_vacuum_paused = 229;
constexpr uint16_t ha_vacuum_returning = 230;
constexpr uint16_t ha_vacuum_idle = 231;
constexpr uint16_t ha_weather_sunny = 232;
constexpr uint16_t ha_weather_clear_night = 233;
constexpr uint16_t ha_weather_cloudy = 234;
constexpr uint16_t ha_weather_partlycloudy = 235;
constexpr uint16_t ha_weather_rainy = 236;
constexpr uint16_t ha_weather_pouring = 237;
constexpr uint16_t ha_weather_snowy = 238;
constexpr uint16_t ha_weather_snowy_rainy = 239;
constexpr uint16_t ha_weather_fog = 240;
constexpr uint16_t ha_weather_hail = 241;
constexpr uint16_t ha_weather_lightning = 242;
constexpr uint16_t ha_weather_lightning_rainy = 243;
constexpr uint16_t ha_weather_windy = 244;
constexpr uint16_t ha_weather_exceptional = 245;
constexpr uint16_t ha_binary_battery_on = 246;
constexpr uint16_t ha_binary_battery_off = 247;
constexpr uint16_t ha_binary_battery_charging_on = 248;
constexpr uint16_t ha_binary_battery_charging_off = 249;
constexpr uint16_t ha_binary_carbon_monoxide_on = 250;
constexpr uint16_t ha_binary_carbon_monoxide_off = 251;
constexpr uint16_t ha_binary_cold_on = 252;
constexpr uint16_t ha_binary_cold_off = 253;
constexpr uint16_t ha_binary_connectivity_on = 254;
constexpr uint16_t ha_binary_connectivity_off = 255;
constexpr uint16_t ha_binary_door_on = 256;
constexpr uint16_t ha_binary_door_off = 257;
constexpr uint16_t ha_binary_garage_door_on = 258;
constexpr uint16_t ha_binary_garage_door_off = 259;
constexpr uint16_t ha_binary_gas_on = 260;
constexpr uint16_t ha_binary_gas_off = 261;
constexpr uint16_t ha_binary_heat_on = 262;
constexpr uint16_t ha_binary_heat_off = 263;
constexpr uint16_t ha_binary_light_on = 264;
constexpr uint16_t ha_binary_light_off = 265;
constexpr uint16_t ha_binary_lock_on = 266;
constexpr uint16_t ha_binary_lock_off = 267;
constexpr uint16_t ha_binary_moisture_on = 268;
constexpr uint16_t ha_binary_moisture_off = 269;
constexpr uint16_t ha_binary_motion_on = 270;
constexpr uint16_t ha_binary_motion_off = 271;
constexpr uint16_t ha_binary_moving_on = 272;
constexpr uint16_t ha_binary_moving_off = 273;
constexpr uint16_t ha_binary_occupancy_on = 274;
constexpr uint16_t ha_binary_occupancy_off = 275;
constexpr uint16_t ha_binary_opening_on = 276;
constexpr uint16_t ha_binary_opening_off = 277;
constexpr uint16_t ha_binary_plug_on = 278;
constexpr uint16_t ha_binary_plug_off = 279;
constexpr uint16_t ha_binary_power_on = 280;
constexpr uint16_t ha_binary_power_off = 281;
constexpr uint16_t ha_binary_presence_on = 282;
constexpr uint16_t ha_binary_presence_off = 283;
constexpr uint16_t ha_binary_problem_on = 284;
constexpr uint16_t ha_binary_problem_off = 285;
constexpr uint16_t ha_binary_running_on = 286;
constexpr uint16_t ha_binary_running_off = 287;
constexpr uint16_t ha_binary_safety_on = 288;
constexpr uint16_t ha_binary_safety_off = 289;
constexpr uint16_t ha_binary_smoke_on = 290;
constexpr uint16_t ha_binary_smoke_off = 291;
constexpr uint16_t ha_binary_sound_on = 292;
constexpr uint16_t ha_binary_sound_off = 293;
constexpr uint16_t ha_binary_tamper_on = 294;
constexpr uint16_t ha_binary_tamper_off = 295;
constexpr uint16_t ha_binary_update_on = 296;
constexpr uint16_t ha_binary_update_off = 297;
constexpr uint16_t ha_binary_vibration_on = 298;
constexpr uint16_t ha_binary_vibration_off = 299;
constexpr uint16_t ha_binary_window_on = 300;
constexpr uint16_t ha_binary_window_off = 301;
constexpr uint16_t ha_button_activate = 302;
constexpr uint16_t ha_button_run = 303;
constexpr uint16_t ha_button_press = 304;
constexpr uint16_t date_weekdays = 17;
constexpr uint16_t number_percent = 3;
constexpr uint16_t time_just_now = 4;
constexpr uint16_t time_minutes_ago = 5;
constexpr uint16_t time_hours_ago = 6;
constexpr uint16_t time_yesterday = 7;
constexpr uint16_t time_days_ago = 8;
constexpr uint16_t time_weeks_ago = 9;
constexpr uint16_t time_months_ago = 10;
constexpr uint16_t time_years_ago = 11;
constexpr uint16_t time_in_minutes = 12;
constexpr uint16_t time_in_hours = 13;
constexpr uint16_t time_tomorrow = 14;
constexpr uint16_t time_in_days = 15;
constexpr uint16_t time_am = 16;
constexpr uint16_t time_pm = 17;
constexpr uint16_t date_top_bar = 63;
constexpr uint16_t date_full = 64;
constexpr uint16_t date_day_month = 65;
constexpr uint16_t date_weekday_time = 66;
constexpr uint16_t duration_seconds = 67;
constexpr uint16_t duration_minutes = 68;
constexpr uint16_t duration_hours = 69;
constexpr uint16_t duration_hours_minutes = 70;
constexpr uint16_t duration_days = 71;
constexpr uint16_t duration_days_hours = 72;
constexpr uint16_t status_connecting = 73;
constexpr uint16_t status_waiting = 74;
constexpr uint16_t status_loading_tiles = 75;
constexpr uint16_t status_ha_not_connected = 76;
constexpr uint16_t status_manager_not_active = 77;
constexpr uint16_t status_choose_tiles = 78;
constexpr uint16_t status_home = 79;
constexpr uint16_t status_no_network = 80;
constexpr uint16_t settings_title = 81;
constexpr uint16_t settings_brightness = 82;
constexpr uint16_t settings_night = 83;
constexpr uint16_t settings_screen = 84;
constexpr uint16_t settings_this_screen = 85;
constexpr uint16_t settings_dark_mode = 86;
constexpr uint16_t settings_auto_standby = 87;
constexpr uint16_t settings_standby_after = 88;
constexpr uint16_t settings_standby_brightness = 89;
constexpr uint16_t settings_night_mode = 90;
constexpr uint16_t settings_starts = 91;
constexpr uint16_t settings_ends = 92;
constexpr uint16_t settings_night_brightness = 93;
constexpr uint16_t settings_back_to_page_1 = 94;
constexpr uint16_t settings_after = 95;
constexpr uint16_t settings_also_on_standby = 96;
constexpr uint16_t settings_swipe_between_pages = 97;
constexpr uint16_t settings_page_buttons = 98;
constexpr uint16_t settings_rotation = 99;
constexpr uint16_t settings_address = 100;
constexpr uint16_t settings_firmware = 101;
constexpr uint16_t settings_home_assistant = 102;
constexpr uint16_t settings_restart = 103;
constexpr uint16_t settings_connected = 104;
constexpr uint16_t settings_not_connected = 105;
constexpr uint16_t settings_tap_again_to_restart = 106;
constexpr uint16_t settings_seconds = 107;
constexpr uint16_t settings_minutes = 108;
constexpr uint16_t settings_hours = 109;
constexpr uint16_t settings_hours_minutes = 110;
constexpr uint16_t tile_tap_to_open = 111;
constexpr uint16_t tile_page = 112;
constexpr uint16_t tile_refused = 113;
constexpr uint16_t tile_command_sent = 114;
constexpr uint16_t tile_confirmed = 115;
constexpr uint16_t alert_notification = 116;
constexpr uint16_t alert_ok = 117;
constexpr uint16_t light_color = 118;
constexpr uint16_t light_color_temperature = 119;
constexpr uint16_t light_brightness = 120;
constexpr uint16_t light_waiting = 121;
constexpr uint16_t effects_effect = 122;
constexpr uint16_t effects_nothing = 123;
constexpr uint16_t effects_loading = 124;
constexpr uint16_t effects_not_connected = 125;
constexpr uint16_t climate_target = 126;
constexpr uint16_t climate_mode = 127;
constexpr uint16_t climate_fan = 128;
constexpr uint16_t climate_swing = 129;
constexpr uint16_t climate_now = 130;
constexpr uint16_t vacuum_check_robot = 131;
constexpr uint16_t vacuum_auto_suction = 132;
constexpr uint16_t vacuum_robot_chooses = 133;
constexpr uint16_t vacuum_per_room = 134;
constexpr uint16_t vacuum_speed_quiet = 135;
constexpr uint16_t vacuum_speed_balanced = 136;
constexpr uint16_t vacuum_speed_turbo = 137;
constexpr uint16_t vacuum_speed_max = 138;
constexpr uint16_t vacuum_pause_cleaning = 139;
constexpr uint16_t vacuum_pause = 140;
constexpr uint16_t vacuum_resume = 141;
constexpr uint16_t vacuum_start_cleaning = 142;
constexpr uint16_t vacuum_clean = 143;
constexpr uint16_t vacuum_dock = 144;
constexpr uint16_t cover_position = 145;
constexpr uint16_t cover_tilt = 146;
constexpr uint16_t cover_tilt_value = 147;
constexpr uint16_t weather_feels_like = 148;
constexpr uint16_t weather_no_forecast = 149;
constexpr uint16_t weather_coming_days = 150;
constexpr uint16_t history_no_data = 151;
constexpr uint16_t history_average = 152;
constexpr uint16_t history_average_of = 153;
constexpr uint16_t history_now = 154;
constexpr uint16_t history_range_hour = 155;
constexpr uint16_t history_range_day = 156;
constexpr uint16_t history_range_week = 157;
constexpr uint16_t history_high = 158;
constexpr uint16_t history_low = 159;
constexpr uint16_t history_times = 160;
constexpr uint16_t history_loading = 161;
constexpr uint16_t history_empty = 162;
constexpr uint16_t history_unavailable = 163;
constexpr uint16_t media_muted = 164;
constexpr uint16_t media_not_playing = 165;
constexpr uint16_t media_loading = 166;
constexpr uint16_t timer_running = 167;
constexpr uint16_t timer_paused = 168;
constexpr uint16_t timer_stopped = 169;
constexpr uint16_t timer_pause = 170;
constexpr uint16_t timer_start = 171;
constexpr uint16_t timer_cancel = 172;
constexpr uint16_t timer_paused_left = 173;
constexpr uint16_t sun_name = 174;
constexpr uint16_t sun_sunrise = 175;
constexpr uint16_t sun_sunset = 176;
constexpr uint16_t sun_rise = 177;
constexpr uint16_t sun_set = 178;
constexpr uint16_t script_running = 179;
constexpr uint16_t script_never_run = 180;
constexpr uint16_t script_last_time = 181;
constexpr uint16_t script_yesterday_time = 182;
constexpr uint16_t script_last_date = 183;
constexpr uint16_t camera_no_image = 184;
constexpr uint16_t camera_loading = 185;
constexpr uint16_t camera_live = 186;
constexpr uint16_t camera_recording = 187;
constexpr uint16_t camera_tap_to_view = 188;
constexpr uint16_t camera_no_image_yet = 189;
constexpr uint16_t calibration_title = 190;
constexpr uint16_t calibration_touch_check = 191;
constexpr uint16_t calibration_hold = 192;
constexpr uint16_t calibration_crosshair = 193;
constexpr uint16_t calibration_gently = 194;
constexpr uint16_t calibration_same = 195;
constexpr uint16_t calibration_rejected = 196;
constexpr uint16_t calibration_save_failed = 197;
constexpr uint16_t ha_on = 198;
constexpr uint16_t ha_off = 199;
constexpr uint16_t ha_unavailable = 200;
constexpr uint16_t ha_climate_off = 201;
constexpr uint16_t ha_climate_heat = 202;
constexpr uint16_t ha_climate_cool = 203;
constexpr uint16_t ha_climate_heat_cool = 204;
constexpr uint16_t ha_climate_auto = 205;
constexpr uint16_t ha_climate_dry = 206;
constexpr uint16_t ha_climate_fan_only = 207;
constexpr uint16_t ha_hvac_action_heating = 208;
constexpr uint16_t ha_hvac_action_cooling = 209;
constexpr uint16_t ha_hvac_action_idle = 210;
constexpr uint16_t ha_hvac_action_off = 211;
constexpr uint16_t ha_hvac_action_drying = 212;
constexpr uint16_t ha_hvac_action_fan = 213;
constexpr uint16_t ha_hvac_action_preheating = 214;
constexpr uint16_t ha_hvac_action_defrosting = 215;
constexpr uint16_t ha_cover_open = 216;
constexpr uint16_t ha_cover_closed = 217;
constexpr uint16_t ha_cover_opening = 218;
constexpr uint16_t ha_cover_closing = 219;
constexpr uint16_t ha_media_playing = 220;
constexpr uint16_t ha_media_paused = 221;
constexpr uint16_t ha_media_idle = 222;
constexpr uint16_t ha_media_standby = 223;
constexpr uint16_t ha_person_home = 224;
constexpr uint16_t ha_person_not_home = 225;
constexpr uint16_t ha_sun_above_horizon = 226;
constexpr uint16_t ha_sun_below_horizon = 227;
constexpr uint16_t ha_vacuum_docked = 228;
constexpr uint16_t ha_vacuum_cleaning = 229;
constexpr uint16_t ha_vacuum_paused = 230;
constexpr uint16_t ha_vacuum_returning = 231;
constexpr uint16_t ha_vacuum_idle = 232;
constexpr uint16_t ha_weather_sunny = 233;
constexpr uint16_t ha_weather_clear_night = 234;
constexpr uint16_t ha_weather_cloudy = 235;
constexpr uint16_t ha_weather_partlycloudy = 236;
constexpr uint16_t ha_weather_rainy = 237;
constexpr uint16_t ha_weather_pouring = 238;
constexpr uint16_t ha_weather_snowy = 239;
constexpr uint16_t ha_weather_snowy_rainy = 240;
constexpr uint16_t ha_weather_fog = 241;
constexpr uint16_t ha_weather_hail = 242;
constexpr uint16_t ha_weather_lightning = 243;
constexpr uint16_t ha_weather_lightning_rainy = 244;
constexpr uint16_t ha_weather_windy = 245;
constexpr uint16_t ha_weather_exceptional = 246;
constexpr uint16_t ha_binary_battery_on = 247;
constexpr uint16_t ha_binary_battery_off = 248;
constexpr uint16_t ha_binary_battery_charging_on = 249;
constexpr uint16_t ha_binary_battery_charging_off = 250;
constexpr uint16_t ha_binary_carbon_monoxide_on = 251;
constexpr uint16_t ha_binary_carbon_monoxide_off = 252;
constexpr uint16_t ha_binary_cold_on = 253;
constexpr uint16_t ha_binary_cold_off = 254;
constexpr uint16_t ha_binary_connectivity_on = 255;
constexpr uint16_t ha_binary_connectivity_off = 256;
constexpr uint16_t ha_binary_door_on = 257;
constexpr uint16_t ha_binary_door_off = 258;
constexpr uint16_t ha_binary_garage_door_on = 259;
constexpr uint16_t ha_binary_garage_door_off = 260;
constexpr uint16_t ha_binary_gas_on = 261;
constexpr uint16_t ha_binary_gas_off = 262;
constexpr uint16_t ha_binary_heat_on = 263;
constexpr uint16_t ha_binary_heat_off = 264;
constexpr uint16_t ha_binary_light_on = 265;
constexpr uint16_t ha_binary_light_off = 266;
constexpr uint16_t ha_binary_lock_on = 267;
constexpr uint16_t ha_binary_lock_off = 268;
constexpr uint16_t ha_binary_moisture_on = 269;
constexpr uint16_t ha_binary_moisture_off = 270;
constexpr uint16_t ha_binary_motion_on = 271;
constexpr uint16_t ha_binary_motion_off = 272;
constexpr uint16_t ha_binary_moving_on = 273;
constexpr uint16_t ha_binary_moving_off = 274;
constexpr uint16_t ha_binary_occupancy_on = 275;
constexpr uint16_t ha_binary_occupancy_off = 276;
constexpr uint16_t ha_binary_opening_on = 277;
constexpr uint16_t ha_binary_opening_off = 278;
constexpr uint16_t ha_binary_plug_on = 279;
constexpr uint16_t ha_binary_plug_off = 280;
constexpr uint16_t ha_binary_power_on = 281;
constexpr uint16_t ha_binary_power_off = 282;
constexpr uint16_t ha_binary_presence_on = 283;
constexpr uint16_t ha_binary_presence_off = 284;
constexpr uint16_t ha_binary_problem_on = 285;
constexpr uint16_t ha_binary_problem_off = 286;
constexpr uint16_t ha_binary_running_on = 287;
constexpr uint16_t ha_binary_running_off = 288;
constexpr uint16_t ha_binary_safety_on = 289;
constexpr uint16_t ha_binary_safety_off = 290;
constexpr uint16_t ha_binary_smoke_on = 291;
constexpr uint16_t ha_binary_smoke_off = 292;
constexpr uint16_t ha_binary_sound_on = 293;
constexpr uint16_t ha_binary_sound_off = 294;
constexpr uint16_t ha_binary_tamper_on = 295;
constexpr uint16_t ha_binary_tamper_off = 296;
constexpr uint16_t ha_binary_update_on = 297;
constexpr uint16_t ha_binary_update_off = 298;
constexpr uint16_t ha_binary_vibration_on = 299;
constexpr uint16_t ha_binary_vibration_off = 300;
constexpr uint16_t ha_binary_window_on = 301;
constexpr uint16_t ha_binary_window_off = 302;
constexpr uint16_t ha_button_activate = 303;
constexpr uint16_t ha_button_run = 304;
constexpr uint16_t ha_button_press = 305;
constexpr uint16_t date_weekdays = 18;
constexpr uint16_t date_weekdays_count = 7;
constexpr uint16_t date_weekdays_short = 24;
constexpr uint16_t date_weekdays_short = 25;
constexpr uint16_t date_weekdays_short_count = 7;
constexpr uint16_t date_weekdays_min = 31;
constexpr uint16_t date_weekdays_min = 32;
constexpr uint16_t date_weekdays_min_count = 7;
constexpr uint16_t date_months = 38;
constexpr uint16_t date_months = 39;
constexpr uint16_t date_months_count = 12;
constexpr uint16_t date_months_short = 50;
constexpr uint16_t date_months_short = 51;
constexpr uint16_t date_months_short_count = 12;
} // namespace txt
constexpr uint16_t KEY_COUNT = 305;
constexpr uint32_t KEYS_HASH = 0xC6B1A3EDu;
constexpr uint16_t KEY_COUNT = 306;
constexpr uint32_t KEYS_HASH = 0x5DBB4605u;
} // namespace screen_text
+5 -9
View File
@@ -190,21 +190,17 @@ inline std::string duration_text(int32_t seconds) {
}
// Minutes since midnight as the clock on this screen shows them.
inline std::string moment_text(int32_t minutes, bool clock_24h) {
char buffer[16];
int hour = minutes / 60, minute = minutes % 60;
if (clock_24h) snprintf(buffer, sizeof(buffer), "%02d:%02d", hour, minute);
else snprintf(buffer, sizeof(buffer), "%d:%02d %s", hour % 12 ? hour % 12 : 12, minute,
screen_text::tr(hour < 12 ? screen_text::txt::time_am : screen_text::txt::time_pm));
return buffer;
char buffer[8];
snprintf(buffer, sizeof(buffer), "%02d:%02d", (int) (minutes / 60 % 24), (int) (minutes % 60));
return screen_text::clock_text(buffer, clock_24h);
}
// What the right-hand side of a row says. A toggle draws a switch instead, its text is for the tests.
inline std::string value_text(const Row &row) {
switch (row.kind) {
case Kind::toggle: return screen_text::tr(row.read && row.read() ? screen_text::txt::ha_on : screen_text::txt::ha_off);
case Kind::number: {
char buffer[24];
snprintf(buffer, sizeof(buffer), "%d%s", row.read ? (int) row.read() : 0, row.unit);
return buffer;
int value = row.read ? (int) row.read() : 0;
return strcmp(row.unit, "%") == 0 ? screen_text::percent(value) : std::to_string(value) + row.unit;
}
case Kind::duration: return duration_text(row.read ? row.read() : 0);
case Kind::moment: return moment_text(row.read ? row.read() : 0, screen_settings::current.clock_24h != 0);
+3 -3
View File
@@ -243,7 +243,7 @@ inline const char *binary_state_text(const std::string &device_class, bool on) {
}
// Status line beside a control panel: what Home Assistant shows under the name.
inline std::string status_text(const Tile &t) {
auto d = t.domain(); char b[48];
auto d = t.domain();
if (d == "climate") {
std::string text = climate_action_text(t.extra().hvac_action);
if (text.empty()) text = climate_mode_text(t.state);
@@ -252,13 +252,13 @@ inline std::string status_text(const Tile &t) {
}
if (d == "cover") {
std::string text = cover_state_text(t.state);
if (std::isfinite(t.position)) { snprintf(b, sizeof(b), " · %d%%", (int) std::lround(t.position)); text += b; }
if (std::isfinite(t.position)) text += " · " + screen_text::percent((int) std::lround(t.position));
return text;
}
if (d == "media_player") {
const std::string &title = t.extra().media_title;
std::string text = (t.state == "playing" || t.state == "paused") && !title.empty() ? title : media_state_text(t.state);
if (std::isfinite(t.volume) && (t.supported & feature::MEDIA_VOLUME_SET)) { snprintf(b, sizeof(b), " · %d%%", (int) std::lround(t.volume * 100)); text += b; }
if (std::isfinite(t.volume) && (t.supported & feature::MEDIA_VOLUME_SET)) text += " · " + screen_text::percent((int) std::lround(t.volume * 100));
return text;
}
return {};