Build each screen in one language: the screen texts from the translations, European letters in the fonts, and Language & region in the app
The firmware takes its texts from screen_manager/translations/<code>.json at build time (components/smart_display: one table in main.cpp, keys in screen_text_keys.h), and the fonts carry every letter European languages write with. The app keeps Settings -> Language & region (language, clock, numbers) for every screen, writes LANGUAGE into the screens' YAML and sends the clock and the number format in the layout message; the per-screen 24-hour clock goes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,38 @@
|
||||
"""Runtime tile configuration transported through the normal ESPHome API."""
|
||||
"""Runtime tile configuration transported through the normal ESPHome API.
|
||||
|
||||
`smart_display: language: nl` (app 0.2.90) builds the screen's texts in that language: the `screen` section of
|
||||
screen_manager/translations/nl.json, with English for any key it lacks, becomes one table in main.cpp
|
||||
(screen_text_gen.py, screen_text.h). The shared core sets it from the LANGUAGE substitution, which ESP Screens writes
|
||||
into a screen's YAML.
|
||||
"""
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from . import screen_text_gen
|
||||
|
||||
CODEOWNERS = []
|
||||
AUTO_LOAD = ["json"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_LANGUAGE = "language"
|
||||
|
||||
|
||||
def _language(value):
|
||||
value = cv.string_strict(value).strip()
|
||||
if not screen_text_gen.LANGUAGE_CODE.fullmatch(value):
|
||||
raise cv.Invalid("Expected a language code such as en, nl or pt-BR")
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({cv.Optional(CONF_LANGUAGE, default="en"): _language})
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
wanted = config[CONF_LANGUAGE]
|
||||
language, code = screen_text_gen.definitions(wanted)
|
||||
if language != wanted:
|
||||
# ESP Screens only writes languages it has; a hand-written one it lacks builds in the nearest it does have.
|
||||
_LOGGER.warning("No translation for %s yet; the screen's texts are in %s", wanted, language)
|
||||
cg.add_global(cg.RawStatement(code))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include "screen_text.h"
|
||||
#include "tile_icon.h"
|
||||
#include "tile_icon_names.h"
|
||||
#include "tile_palette.h"
|
||||
@@ -11,7 +12,6 @@
|
||||
// tests/test_alert_overlay.cpp can check them; the profiles do the LVGL work.
|
||||
namespace screen_alert {
|
||||
constexpr const char *FALLBACK_ICON = "alert-outline";
|
||||
constexpr const char *FALLBACK_TITLE = "Notification";
|
||||
constexpr const char *FALLBACK_BUTTON = "OK";
|
||||
constexpr uint32_t DEFAULT_CARD_COLOR = 0; // the normal card: theme::surface(0)
|
||||
constexpr int MAX_TIMEOUT_SECONDS = 86400;
|
||||
@@ -59,7 +59,7 @@ inline Alert make(const std::string &title, const std::string &subtitle, const s
|
||||
size_t title_max, size_t subtitle_max, size_t button_max) {
|
||||
Alert alert;
|
||||
alert.title = clipped(trimmed(title), title_max);
|
||||
if (alert.title.empty()) alert.title = FALLBACK_TITLE;
|
||||
if (alert.title.empty()) alert.title = screen_text::tr(screen_text::txt::alert_notification);
|
||||
alert.subtitle = clipped(trimmed(subtitle), subtitle_max);
|
||||
alert.button = clipped(trimmed(button), button_max);
|
||||
if (alert.button.empty()) alert.button = FALLBACK_BUTTON;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "lvgl.h"
|
||||
#include "theme.h"
|
||||
#include "screen_text.h"
|
||||
#include <functional>
|
||||
namespace screen_calibration {
|
||||
inline bool active = false, pressed = false;
|
||||
@@ -15,9 +16,10 @@ inline std::array<std::array<Point,3>,5> samples;
|
||||
inline esphome::ESPPreferenceObject preference;
|
||||
inline std::function<void(const Calibration &)> apply;
|
||||
inline std::function<void(bool)> isolation;
|
||||
inline void prompt(const char *message = "Hold each tap for a moment") {
|
||||
inline void prompt(const char *message = nullptr) {
|
||||
if (!message) message = screen_text::tr(screen_text::txt::calibration_hold);
|
||||
lv_obj_set_pos(cross, TARGETS[point].x-12, TARGETS[point].y-12);
|
||||
lv_label_set_text_fmt(instructions, "Tap the crosshair\nPoint %u/5 - tap %u/3", point+1, sample+1);
|
||||
lv_label_set_text(instructions, screen_text::fill(screen_text::fill(screen_text::txt::calibration_crosshair, "point", (int) point + 1), "tap", std::to_string(sample + 1)).c_str());
|
||||
lv_label_set_text(status, message);
|
||||
}
|
||||
inline void begin() {
|
||||
@@ -55,13 +57,13 @@ inline void press() { if (active) { down_at = esphome::millis(); pressed = true;
|
||||
inline void release(int raw_x, int raw_y) {
|
||||
if (!active || !pressed) return;
|
||||
pressed = false;
|
||||
if (esphome::millis()-down_at < 80 || raw_x < 0 || raw_x > 4095 || raw_y < 0 || raw_y > 4095) { prompt("Tap gently, a bit longer"); return; }
|
||||
if (esphome::millis()-down_at < 80 || raw_x < 0 || raw_x > 4095 || raw_y < 0 || raw_y > 4095) { prompt(screen_text::tr(screen_text::txt::calibration_gently)); return; }
|
||||
samples[point][sample++] = {double(raw_x), double(raw_y)};
|
||||
if (sample < 3) { prompt(); return; }
|
||||
double xs[3], ys[3];
|
||||
for (int i=0;i<3;++i) { xs[i]=samples[point][i].x; ys[i]=samples[point][i].y; }
|
||||
std::sort(xs,xs+3); std::sort(ys,ys+3);
|
||||
if (xs[2]-xs[0]>180 || ys[2]-ys[0]>180) { sample=0; prompt("Tap the same crosshair 3 times"); return; }
|
||||
if (xs[2]-xs[0]>180 || ys[2]-ys[0]>180) { sample=0; prompt(screen_text::tr(screen_text::txt::calibration_same)); return; }
|
||||
point++; sample=0;
|
||||
if (point < 5) { prompt(); return; }
|
||||
std::array<Point,5> raw;
|
||||
@@ -70,8 +72,8 @@ inline void release(int raw_x, int raw_y) {
|
||||
std::sort(xs,xs+3);std::sort(ys,ys+3);raw[n]={xs[1],ys[1]};
|
||||
}
|
||||
Calibration next = calibration;
|
||||
if (!fit(raw,next)) { point=0;prompt("Measurement rejected; try again");return; }
|
||||
if (!preference.save(&next) || !esphome::global_preferences->sync()) { point=0;prompt("Save failed; try again");return; }
|
||||
if (!fit(raw,next)) { point=0;prompt(screen_text::tr(screen_text::txt::calibration_rejected));return; }
|
||||
if (!preference.save(&next) || !esphome::global_preferences->sync()) { point=0;prompt(screen_text::tr(screen_text::txt::calibration_save_failed));return; }
|
||||
calibration=next;
|
||||
if (apply) apply(calibration);
|
||||
active=false;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "runtime_model.h"
|
||||
#include "screen_text.h"
|
||||
|
||||
// A light's effects page (firmware 0.2.70+, app 0.2.83+): the modes of a lamp such as a WLED, reached from the
|
||||
// sparkles key at the top right of the light's colour card. One card of rows, each the name of something the lamp
|
||||
@@ -273,7 +274,7 @@ inline void received(const std::string &for_entity, unsigned page, unsigned page
|
||||
options.pages = std::max(1u, pages);
|
||||
if (options.next < options.pages && options.names.size() < MAX_NAMES) { options.asked_at = clock(); if (ask) ask(options.entity, options.next); return; }
|
||||
options.complete = true;
|
||||
if (options.names.empty()) { if (roller_note) lv_label_set_text(roller_note, "Nothing to choose"); return; }
|
||||
if (options.names.empty()) { if (roller_note) lv_label_set_text(roller_note, screen_text::tr(screen_text::txt::effects_nothing)); return; }
|
||||
show_roller();
|
||||
}
|
||||
inline void picker_back(lv_event_t *) { if (steady()) close_picker(); }
|
||||
@@ -287,7 +288,7 @@ inline void open_picker(int index) {
|
||||
top_bar(picker, m, row.name, picker_back, "\U000F012C", confirm_event);
|
||||
const int w = m.width - 2 * m.pad, h = m.roller_rows * m.roller_row_h + 2 * m.roller_pad;
|
||||
auto *holder = card(picker, m.pad, m.rows_y, w, h, m.radius);
|
||||
roller_note = text(holder, "Loading...", row_font, theme::MUTED, LV_TEXT_ALIGN_CENTER);
|
||||
roller_note = text(holder, screen_text::tr(screen_text::txt::effects_loading), row_font, theme::MUTED, LV_TEXT_ALIGN_CENTER);
|
||||
lv_obj_set_width(roller_note, w - 2 * m.inset);
|
||||
lv_obj_center(roller_note);
|
||||
// The names of the same entity from a moment ago serve again; anything else is asked for afresh.
|
||||
@@ -296,7 +297,7 @@ inline void open_picker(int index) {
|
||||
options.entity = row.entity;
|
||||
options.asked_at = clock();
|
||||
if (ask) ask(row.entity, 0);
|
||||
else lv_label_set_text(roller_note, "Not connected");
|
||||
else lv_label_set_text(roller_note, screen_text::tr(screen_text::txt::effects_not_connected));
|
||||
}
|
||||
|
||||
// ---- the page ----
|
||||
@@ -331,7 +332,7 @@ inline void draw() {
|
||||
// The rows: the light's effect first, then the selects of its device.
|
||||
struct Spec { std::string entity, name, current, fallback; uint32_t icon; bool light; };
|
||||
std::vector<Spec> specs;
|
||||
if (t && (t->supported & EFFECT_FEATURE)) specs.push_back({t->entity, "Effect", t->extra().effect, "\U000F0674", 0, true});
|
||||
if (t && (t->supported & EFFECT_FEATURE)) specs.push_back({t->entity, screen_text::tr(screen_text::txt::effects_effect), t->extra().effect, "\U000F0674", 0, true});
|
||||
if (t) for (auto &r : t->extra().option_rows) specs.push_back({r.entity, r.name, r.current, "\U000F0411", r.icon, false});
|
||||
int y = m.rows_y;
|
||||
if (!specs.empty()) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include "screen_text.h"
|
||||
|
||||
namespace header_bar {
|
||||
// The top bar right of the screen name (firmware 0.2.32+). ESP Screen Manager decides what shows
|
||||
@@ -65,33 +66,35 @@ inline uint32_t next_codepoint(const std::string &s, size_t &i) {
|
||||
}
|
||||
|
||||
// Relative time in the editor's words (app.js agoText): "Just now", "5 min ago", "Yesterday",
|
||||
// "In 2 hours". `now` 0 means the clock is not set yet.
|
||||
// "In 2 hours", in the screen's language (screen.time, app 0.2.90). `now` 0 means the clock is not set yet.
|
||||
inline std::string ago_text(int64_t then, int64_t now) {
|
||||
using namespace screen_text;
|
||||
if (now <= 0 || then <= 0) return "—";
|
||||
int64_t seconds = now - then, span = seconds < 0 ? -seconds : seconds;
|
||||
auto n = [&](int64_t unit) { return std::to_string(span / unit); };
|
||||
auto n = [&](int64_t unit) { return static_cast<int>(span / unit); };
|
||||
if (seconds < 0) {
|
||||
if (span < 3600) return "In " + std::to_string(std::max<int64_t>(1, span / 60)) + " min";
|
||||
if (span < 86400) return span / 3600 == 1 ? "In 1 hour" : "In " + n(3600) + " hours";
|
||||
if (span < 172800) return "Tomorrow";
|
||||
return "In " + n(86400) + " days";
|
||||
if (span < 3600) return fill(txt::time_in_minutes, "n", std::max(1, n(60)));
|
||||
if (span < 86400) return plural(txt::time_in_hours, n(3600));
|
||||
if (span < 172800) return tr(txt::time_tomorrow);
|
||||
return plural(txt::time_in_days, n(86400));
|
||||
}
|
||||
if (span < 60) return "Just now";
|
||||
if (span < 3600) return n(60) + " min ago";
|
||||
if (span < 86400) return span / 3600 == 1 ? "1 hour ago" : n(3600) + " hours ago";
|
||||
if (span < 172800) return "Yesterday";
|
||||
if (span < 604800) return n(86400) + " days ago";
|
||||
if (span < 2592000) return span / 604800 == 1 ? "1 week ago" : n(604800) + " weeks ago";
|
||||
if (span < 31536000) return span / 2592000 == 1 ? "1 month ago" : n(2592000) + " months ago";
|
||||
return span / 31536000 == 1 ? "1 year ago" : n(31536000) + " years ago";
|
||||
if (span < 60) return tr(txt::time_just_now);
|
||||
if (span < 3600) return fill(txt::time_minutes_ago, "n", n(60));
|
||||
if (span < 86400) return plural(txt::time_hours_ago, n(3600));
|
||||
if (span < 172800) return tr(txt::time_yesterday);
|
||||
if (span < 604800) return plural(txt::time_days_ago, n(86400));
|
||||
if (span < 2592000) return plural(txt::time_weeks_ago, n(604800));
|
||||
if (span < 31536000) return plural(txt::time_months_ago, n(2592000));
|
||||
return plural(txt::time_years_ago, n(31536000));
|
||||
}
|
||||
|
||||
// "Mo 14 Sep"; day_of_week 1 is Sunday, as ESPHome counts.
|
||||
// "Mo 14 Sep" in English, "ma 14 sep" in Dutch (screen.date.top_bar); day_of_week 1 is Sunday, as ESPHome counts.
|
||||
inline std::string date_text(int day_of_week, int day_of_month, int month) {
|
||||
static const char *days[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"};
|
||||
static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
||||
using namespace screen_text;
|
||||
if (day_of_week < 1 || day_of_week > 7 || month < 1 || month > 12) return "—";
|
||||
return std::string(days[day_of_week - 1]) + " " + std::to_string(day_of_month) + " " + months[month - 1];
|
||||
std::string text = fill(txt::date_top_bar, "weekday", tr(txt::date_weekdays_min + day_of_week - 1));
|
||||
text = fill(text, "day", std::to_string(day_of_month));
|
||||
return fill(text, "month", tr(txt::date_months_short + month - 1));
|
||||
}
|
||||
|
||||
// What you see between icon and value, between two items and after the name, from the height of
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "screen_text.h"
|
||||
|
||||
namespace history_view {
|
||||
constexpr int PARTS = 24;
|
||||
@@ -22,18 +23,20 @@ struct Run {
|
||||
};
|
||||
|
||||
// A clock time of a unix time in the screen's time zone (`offset` seconds east of UTC), as the screen's 12- or
|
||||
// 24-hour setting writes it; `weekday` puts the day in front ("Tue 14:05") for a week.
|
||||
// 24-hour setting writes it; `weekday` puts the day in front ("Tue 14:05") for a week, in the screen's language.
|
||||
inline std::string clock(int64_t epoch, int32_t offset, bool h24, bool weekday) {
|
||||
using namespace screen_text;
|
||||
int64_t local = epoch + offset;
|
||||
int64_t days = local >= 0 ? local / 86400 : (local - 86399) / 86400;
|
||||
int seconds = static_cast<int>(local - days * 86400);
|
||||
int hour = seconds / 3600, minute = seconds % 3600 / 60;
|
||||
static const char *const names[] = {"Thu", "Fri", "Sat", "Sun", "Mon", "Tue", "Wed"};
|
||||
char b[24];
|
||||
if (h24) snprintf(b, sizeof(b), "%02d:%02d", hour, minute);
|
||||
else snprintf(b, sizeof(b), "%d:%02d %s", hour % 12 ? hour % 12 : 12, minute, hour < 12 ? "AM" : "PM");
|
||||
else snprintf(b, sizeof(b), "%d:%02d %s", hour % 12 ? hour % 12 : 12, minute, tr(hour < 12 ? txt::time_am : txt::time_pm));
|
||||
if (!weekday) return b;
|
||||
return std::string(names[((days % 7) + 7) % 7]) + " " + b;
|
||||
// Day 0 of the unix epoch was a Thursday; the names count from Sunday.
|
||||
int sunday_first = static_cast<int>((((days + 4) % 7) + 7) % 7);
|
||||
return fill(fill(txt::date_weekday_time, "weekday", tr(txt::date_weekdays_short + sunday_first)), "time", b);
|
||||
}
|
||||
|
||||
// A time under a graph: "06:00" on a 24-hour clock, "6 AM" or "6:15 AM" on a 12-hour one.
|
||||
@@ -45,25 +48,25 @@ inline std::string axis_clock(int64_t epoch, int32_t offset, bool h24) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// "45 s", "12 min", "3 h 20 min", "2 d 4 h": the time spent in a state.
|
||||
// "45 s", "12 min", "3 h 20 min", "2 d 4 h": the time spent in a state, in the screen's language (screen.duration).
|
||||
inline std::string duration(uint32_t seconds) {
|
||||
char b[32];
|
||||
if (seconds < 60) snprintf(b, sizeof(b), "%u s", static_cast<unsigned>(seconds));
|
||||
else if (seconds < 3600) snprintf(b, sizeof(b), "%u min", static_cast<unsigned>(seconds / 60));
|
||||
else if (seconds < 86400) {
|
||||
unsigned minutes = seconds / 60 % 60;
|
||||
if (minutes) snprintf(b, sizeof(b), "%u h %u min", static_cast<unsigned>(seconds / 3600), minutes);
|
||||
else snprintf(b, sizeof(b), "%u h", static_cast<unsigned>(seconds / 3600));
|
||||
} else {
|
||||
unsigned hours = seconds / 3600 % 24;
|
||||
if (hours) snprintf(b, sizeof(b), "%u d %u h", static_cast<unsigned>(seconds / 86400), hours);
|
||||
else snprintf(b, sizeof(b), "%u d", static_cast<unsigned>(seconds / 86400));
|
||||
using namespace screen_text;
|
||||
const int s = static_cast<int>(seconds);
|
||||
if (s < 60) return fill(txt::duration_seconds, "n", s);
|
||||
if (s < 3600) return fill(txt::duration_minutes, "n", s / 60);
|
||||
if (s < 86400) {
|
||||
int minutes = s / 60 % 60;
|
||||
if (!minutes) return fill(txt::duration_hours, "n", s / 3600);
|
||||
return fill(fill(txt::duration_hours_minutes, "h", s / 3600), "m", std::to_string(minutes));
|
||||
}
|
||||
return b;
|
||||
int hours = s / 3600 % 24;
|
||||
if (!hours) return fill(txt::duration_days, "n", s / 86400);
|
||||
return fill(fill(txt::duration_days_hours, "d", s / 86400), "h", std::to_string(hours));
|
||||
}
|
||||
|
||||
// A value as Home Assistant writes it: fixed decimals, commas between thousands, "%" and "°" on the number and
|
||||
// any other unit after a space.
|
||||
// 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.
|
||||
inline std::string number(float value, int decimals, const std::string &unit) {
|
||||
if (!std::isfinite(value)) return "--";
|
||||
char b[40];
|
||||
@@ -72,8 +75,10 @@ inline std::string number(float value, int decimals, const std::string &unit) {
|
||||
if (text[0] == '-') { sign = "-"; text.erase(0, 1); }
|
||||
auto dot = text.find('.');
|
||||
std::string whole = text.substr(0, dot), rest = dot == std::string::npos ? "" : text.substr(dot);
|
||||
for (int i = static_cast<int>(whole.size()) - 3; i > 0; i -= 3) whole.insert(static_cast<size_t>(i), ",");
|
||||
bool zero = whole.find_first_not_of("0,") == std::string::npos && rest.find_first_not_of(".0") == std::string::npos;
|
||||
bool zero = whole.find_first_not_of('0') == std::string::npos && rest.find_first_not_of(".0") == std::string::npos;
|
||||
const std::string group = screen_text::group_mark();
|
||||
for (int i = static_cast<int>(whole.size()) - 3; i > 0; i -= 3) whole.insert(static_cast<size_t>(i), group);
|
||||
if (!rest.empty()) rest[0] = screen_text::decimal_mark();
|
||||
text = (zero ? "" : sign) + whole + rest;
|
||||
if (unit.empty()) return text;
|
||||
if (unit == "%" || unit == "°") return text + unit;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include "screen_text.h"
|
||||
|
||||
namespace light_controls {
|
||||
inline int clamp(int value, int low, int high) { return std::max(low, std::min(high, value)); }
|
||||
@@ -93,7 +94,7 @@ inline void preview(Row &row) {
|
||||
lv_obj_set_style_bg_opa(row.slider, row.off ? LV_OPA_TRANSP : LV_OPA_COVER, LV_PART_INDICATOR);
|
||||
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, "Off");
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -182,7 +183,7 @@ inline void setup(lv_obj_t *parent, const lv_font_t *font, int width, int height
|
||||
int margin = large ? 20 : 12, w = width - 2 * margin, card_h = spacing - (large ? 12 : 5);
|
||||
int inset = large ? 18 : 10, text_y = large ? 14 : 5, track_h = large ? 32 : 18;
|
||||
int track_x = inset, track_w = w - 2 * inset, track_y = card_h - inset + (large ? 2 : 3) - track_h, radius = track_h / 2;
|
||||
const char *names[] = {"Color", "Color temperature", "Brightness"};
|
||||
const char *names[] = {screen_text::tr(screen_text::txt::light_color), screen_text::tr(screen_text::txt::light_color_temperature), screen_text::tr(screen_text::txt::light_brightness)};
|
||||
const char *icons[] = {"\U000F03D8", "\U000F050F", "\U000F0335"};
|
||||
for (unsigned i = 0; i < 3; ++i) {
|
||||
auto &row = rows[i]; row.index = i;
|
||||
@@ -287,7 +288,7 @@ inline void open(const std::string &entity, bool color, bool temperature, int br
|
||||
lv_slider_set_value(row.slider, i == 0 ? active->hue : i == 1 ? active->kelvin : clamp(brightness, 1, 100), LV_ANIM_OFF);
|
||||
row.off = i == 2 && brightness <= 0;
|
||||
preview(row); row.dirty = false;
|
||||
if (i == 1 && !active->temperature_ready()) lv_label_set_text(row.value, "Waiting...");
|
||||
if (i == 1 && !active->temperature_ready()) lv_label_set_text(row.value, screen_text::tr(screen_text::txt::light_waiting));
|
||||
}
|
||||
}
|
||||
inline bool self_test() {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include "screen_text.h"
|
||||
|
||||
namespace media_card {
|
||||
struct Rect {
|
||||
@@ -145,11 +146,11 @@ inline std::string subtitle(const std::string &artist, const std::string &album)
|
||||
}
|
||||
// What the card says instead of a title when nothing plays.
|
||||
inline const char *idle_text(const std::string &state) {
|
||||
if (state == "off") return "Off";
|
||||
if (state == "standby") return "Standby";
|
||||
if (state == "buffering") return "Loading";
|
||||
if (state == "unavailable") return "Unavailable";
|
||||
return "Not playing";
|
||||
if (state == "off") return screen_text::tr(screen_text::txt::ha_off);
|
||||
if (state == "standby") return screen_text::tr(screen_text::txt::ha_media_standby);
|
||||
if (state == "buffering") return screen_text::tr(screen_text::txt::media_loading);
|
||||
if (state == "unavailable") return screen_text::tr(screen_text::txt::tile_unavailable);
|
||||
return screen_text::tr(screen_text::txt::media_not_playing);
|
||||
}
|
||||
inline bool playing(const std::string &state) { return state == "playing" || state == "buffering"; }
|
||||
inline bool has_track(const std::string &state) { return state == "playing" || state == "paused" || state == "buffering"; }
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include "screen_text.h"
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -429,7 +430,7 @@ struct Model {
|
||||
// Pages the manager wants shown even when the last ones are still empty (0.2.26+).
|
||||
uint8_t pages = 1;
|
||||
size_t count = 0;
|
||||
std::string title = "Choose tiles in HA";
|
||||
std::string title = screen_text::tr(screen_text::txt::status_choose_tiles);
|
||||
bool configured = false;
|
||||
// Why the last set_layout refused a layout, for the manager's inbox status; empty when it took the layout or the
|
||||
// message itself was wrong (the status then says "invalid message", as before).
|
||||
@@ -469,7 +470,7 @@ struct Model {
|
||||
return false;
|
||||
}
|
||||
// A title-only update must not interrupt an open control card.
|
||||
title = name.empty() ? "Home" : name;
|
||||
title = name.empty() ? std::string(screen_text::tr(screen_text::txt::status_home)) : name;
|
||||
if (changed) {
|
||||
// Freed before the new list is made, so the heap never holds both.
|
||||
tiles.clear();
|
||||
|
||||
@@ -25,6 +25,11 @@
|
||||
#include <algorithm>
|
||||
|
||||
namespace runtime_tiles {
|
||||
// What the screen says, in the language its firmware was built for (screen_text.h, app 0.2.90).
|
||||
using screen_text::fill;
|
||||
using screen_text::plural;
|
||||
using screen_text::tr;
|
||||
namespace txt = screen_text::txt;
|
||||
inline bool enabled = false;
|
||||
// Swiping, rotation and going back to page 1 live in settings_screen, next to the other settings the
|
||||
// screen can change itself; these names stay as the way the rest of the firmware reaches them.
|
||||
@@ -39,6 +44,8 @@ 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.
|
||||
inline esphome::ESPPreferenceObject numbers_preference;
|
||||
inline Model model;
|
||||
inline std::string inbox;
|
||||
inline const lv_font_t *watch_font = nullptr;
|
||||
@@ -144,6 +151,9 @@ inline void load_settings() {
|
||||
home_preference = esphome::global_preferences->make_preference<HomeTimeout>(0x484F4D31);
|
||||
dark_preference = esphome::global_preferences->make_preference<uint32_t>(0x44524B31);
|
||||
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;
|
||||
uint32_t swipe_saved=0;
|
||||
if(swipe_preference.load(&swipe_saved))swipe_pages=swipe_saved==1;
|
||||
uint32_t dark_saved=0;
|
||||
@@ -296,6 +306,10 @@ inline std::string receive(const std::string &payload) {
|
||||
root["auto_home_seconds"].as<unsigned>()<30 || root["auto_home_seconds"].as<unsigned>()>3600))return false;
|
||||
if(!root["rotation"].isNull() && (!root["rotation"].is<unsigned>() ||
|
||||
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.
|
||||
@@ -330,6 +344,12 @@ inline std::string receive(const std::string &payload) {
|
||||
HomeTimeout home{(uint32_t)(auto_home?1:0),(uint32_t)auto_home_seconds};home_preference.save(&home);
|
||||
}
|
||||
bool rotation_changed=false;
|
||||
if(root["clock_24h"].is<bool>())settings.clock_24h=root["clock_24h"].as<bool>()?1:0;
|
||||
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;}
|
||||
}
|
||||
if(rotation_supported && root["rotation"].is<unsigned>() && rotation!=root["rotation"].as<unsigned>()){
|
||||
rotation=root["rotation"].as<unsigned>();rotation_preference.save(&rotation);rotation_changed=true;
|
||||
}
|
||||
@@ -347,7 +367,7 @@ inline std::string receive(const std::string &payload) {
|
||||
// A repeat of the layout on screen (the hourly repeat, a save that changed nothing here) only
|
||||
// refreshes the feed: drawing the whole page again stalls touch input for a few hundred ms,
|
||||
// long enough to spoil a slider drag. The tile states that follow redraw their own tiles.
|
||||
if (changed || moved || !was_configured || rotation_changed || model.pages != previous_pages || model.title != previous_title) {
|
||||
if (changed || moved || !was_configured || rotation_changed || format_changed || model.pages != previous_pages || model.title != previous_title) {
|
||||
if (layout_changed) layout_changed();
|
||||
refresh_all();
|
||||
}
|
||||
@@ -887,20 +907,20 @@ inline lv_obj_t *detail_label(lv_obj_t *parent,const std::string &text,int x,int
|
||||
lv_label_set_long_mode(label,LV_LABEL_LONG_DOT);lv_obj_set_height(label,lv_font_get_line_height(detail_font));return label;
|
||||
}
|
||||
inline std::string detail_state(const Tile &t){
|
||||
if(t.domain()=="person")return t.state=="home"?"Home":t.state=="not_home"?"Away":t.state;
|
||||
if(t.domain()=="sun")return t.state=="above_horizon"?"Above the horizon":"Below the horizon";
|
||||
if(t.domain()=="person")return t.state=="home"?tr(txt::ha_person_home):t.state=="not_home"?tr(txt::ha_person_not_home):t.state;
|
||||
if(t.domain()=="sun")return tr(t.state=="above_horizon"?txt::ha_sun_above_horizon:txt::ha_sun_below_horizon);
|
||||
if(t.domain()=="timer")return timer_text(t);
|
||||
if(t.domain()=="script"||t.domain()=="scene"||t.domain()=="button"||t.domain()=="input_button")return t.state=="on"?"Running...":last_run_text(t.last_run);
|
||||
if(t.domain()=="script"||t.domain()=="scene"||t.domain()=="button"||t.domain()=="input_button")return t.state=="on"?tr(txt::script_running):last_run_text(t.last_run);
|
||||
if(t.domain()=="binary_sensor"&&(t.state=="on"||t.state=="off"))return tile_controls::binary_state_text(t.device_class,t.state=="on");
|
||||
if(t.state=="on")return "On";
|
||||
if(t.state=="off")return "Off";
|
||||
if(t.state=="docked")return "Docked";
|
||||
if(t.state=="cleaning")return "Cleaning";
|
||||
if(t.state=="paused")return "Paused";
|
||||
if(t.state=="returning")return "Returning to dock";
|
||||
if(t.state=="idle")return "Idle";
|
||||
if(t.state=="error")return "Check the robot in HA";
|
||||
if(!t.available())return "Unavailable";
|
||||
if(t.state=="on")return tr(txt::ha_on);
|
||||
if(t.state=="off")return tr(txt::ha_off);
|
||||
if(t.state=="docked")return tr(txt::ha_vacuum_docked);
|
||||
if(t.state=="cleaning")return tr(txt::ha_vacuum_cleaning);
|
||||
if(t.state=="paused")return tr(txt::ha_vacuum_paused);
|
||||
if(t.state=="returning")return tr(txt::ha_vacuum_returning);
|
||||
if(t.state=="idle")return tr(txt::ha_vacuum_idle);
|
||||
if(t.state=="error")return tr(txt::vacuum_check_robot);
|
||||
if(!t.available())return tr(txt::tile_unavailable);
|
||||
// Home Assistant's word where the screen has none of its own (firmware 0.2.58+).
|
||||
if(!t.extra().state_word.empty())return t.extra().state_word;
|
||||
return t.state;
|
||||
@@ -1068,10 +1088,10 @@ inline lv_obj_t *detail_text(lv_obj_t *parent,const std::string &text,int x,int
|
||||
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%% · %.1f mm",(int)std::lround(chance),mm);return b;}
|
||||
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(std::isfinite(mm) && mm>=0.05f){snprintf(b,sizeof(b),mm<10?"%.1f mm":"%.0f mm",mm);return b;}
|
||||
if(std::isfinite(mm) && mm>=0.05f)return screen_text::decimal(mm,mm<10?1:0)+" mm";
|
||||
return "";
|
||||
}
|
||||
inline std::string degrees(float value){ if(!std::isfinite(value))return "--"; char b[16];snprintf(b,sizeof(b),"%.0f°",value);return b; }
|
||||
@@ -1111,9 +1131,9 @@ inline void render_weather_detail(const Tile &t,bool large,int width,int height,
|
||||
detail_text(now,degrees(t.current),temp_x,cy+(hero-big_h)/2,temp_w,big,LV_TEXT_ALIGN_LEFT,ink);
|
||||
int text_x=temp_x+temp_w+(large?4:2),text_w=width-2*pad-card_pad-text_x;
|
||||
int lines_h=text_h+small_h+(large?2:0);
|
||||
detail_text(now,t.available()?weather_text(t.state):"Unavailable",text_x,cy+(hero-lines_h)/2,text_w,detail_font,LV_TEXT_ALIGN_LEFT,ink);
|
||||
detail_text(now,t.available()?weather_text(t.state):tr(txt::tile_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)){snprintf(b,sizeof(b),"Feels like %.0f°",weather.feels);details=b;}
|
||||
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(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);
|
||||
@@ -1130,8 +1150,8 @@ inline void render_weather_detail(const Tile &t,bool large,int width,int height,
|
||||
}
|
||||
y+=card_a_h+(large?12:6);
|
||||
// Coming days: a heading and a card with one row per day.
|
||||
if(!weather.forecast.size()){detail_text(detail_root,"No daily forecast from Home Assistant",pad,y,width-2*pad,small,LV_TEXT_ALIGN_LEFT,muted);return;}
|
||||
if(large){detail_text(detail_root,"Coming days",pad+4,y,width-2*pad,detail_font,LV_TEXT_ALIGN_LEFT,muted);y+=text_h+8;}
|
||||
if(!weather.forecast.size()){detail_text(detail_root,tr(txt::weather_no_forecast),pad,y,width-2*pad,small,LV_TEXT_ALIGN_LEFT,muted);return;}
|
||||
if(large){detail_text(detail_root,tr(txt::weather_coming_days),pad+4,y,width-2*pad,detail_font,LV_TEXT_ALIGN_LEFT,muted);y+=text_h+8;}
|
||||
int card_b_h=height-y-(large?10:4);
|
||||
auto *days=detail_card(pad,y,width-2*pad,card_b_h);
|
||||
int row_pad=large?8:4,row=(card_b_h-2*row_pad)/(int)weather.forecast.size();
|
||||
@@ -1275,7 +1295,7 @@ inline void render_vacuum_detail(Tile &t,bool large,int width,int height,int pad
|
||||
bool cleaning=t.state=="cleaning",paused=t.state=="paused";
|
||||
bool battery=std::isfinite(t.battery),on_the_way=cleaning||paused||t.state=="returning";
|
||||
int inner=width-2*pad,gap=large?12:6,radius=lv_obj_get_style_radius(widgets[0].tile,LV_PART_MAIN);
|
||||
std::string state=t.loading(now)?"Command sent...":detail_state(t);
|
||||
std::string state=t.loading(now)?tr(txt::tile_command_sent):detail_state(t);
|
||||
// A robot with little to set gets a hero on the small screen too; one with mode and water rows uses a status row.
|
||||
bool small_hero=!large && !mode && !t.choice('w');
|
||||
int y=large?92:52;
|
||||
@@ -1314,12 +1334,12 @@ inline void render_vacuum_detail(Tile &t,bool large,int width,int height,int pad
|
||||
}
|
||||
// Start (or pause, or resume) as the one blue button, dock beside it.
|
||||
int action_h=large?54:(small_hero?40:36),start_w=large?(inner-gap)*2/3:(inner-gap)/2;
|
||||
vacuum_command(cleaning?"\U000F03E4":"\U000F040A",cleaning?(large?"Pause cleaning":"Pause"):paused?"Resume":(large?"Start cleaning":"Clean"),
|
||||
vacuum_command(cleaning?"\U000F03E4":"\U000F040A",tr(cleaning?(large?txt::vacuum_pause_cleaning:txt::vacuum_pause):paused?txt::vacuum_resume:(large?txt::vacuum_start_cleaning:txt::vacuum_clean)),
|
||||
pad,y,start_w,action_h,cleaning?1:0,true,text,icons,large?radius:action_h/2);
|
||||
vacuum_command("\U000F05F8","Dock",pad+start_w+gap,y,inner-start_w-gap,action_h,2,false,text,icons,large?radius:action_h/2);
|
||||
vacuum_command("\U000F05F8",tr(txt::vacuum_dock),pad+start_w+gap,y,inner-start_w-gap,action_h,2,false,text,icons,large?radius:action_h/2);
|
||||
y+=action_h+gap;
|
||||
if(!mode && !suction && !water){
|
||||
auto *note=detail_text(detail_root,"Automatic suction power",pad,y+gap,inner,text,LV_TEXT_ALIGN_CENTER,theme::SUBTLE);(void)note;
|
||||
auto *note=detail_text(detail_root,tr(txt::vacuum_auto_suction),pad,y+gap,inner,text,LV_TEXT_ALIGN_CENTER,theme::SUBTLE);(void)note;
|
||||
return;
|
||||
}
|
||||
// How it cleans. The Guition groups the rows on one white card; the small screen has no room for a card.
|
||||
@@ -1340,7 +1360,7 @@ inline void render_vacuum_detail(Tile &t,bool large,int width,int height,int pad
|
||||
if(water)level(*water,60,"\U000F058C");
|
||||
if(automatic){
|
||||
bool smart=tile_controls::shown_value(t,*mode,now).find("smart")!=std::string::npos;
|
||||
detail_text(detail_root,smart?"The robot chooses suction and water":"Suction and water as set per room",x,y,w,text,LV_TEXT_ALIGN_CENTER,theme::SUBTLE);
|
||||
detail_text(detail_root,tr(smart?txt::vacuum_robot_chooses:txt::vacuum_per_room),x,y,w,text,LV_TEXT_ALIGN_CENTER,theme::SUBTLE);
|
||||
}
|
||||
}
|
||||
// ---- Cover card (firmware 0.2.50+): Home Assistant's cover dialog in the style of the vacuum and climate cards ----
|
||||
@@ -1353,7 +1373,7 @@ inline constexpr uint32_t COVER_ACCENT = theme::ha::PURPLE;
|
||||
inline uint32_t cover_track(){return theme::tint(COVER_ACCENT,37);}
|
||||
inline uint32_t cover_slats(){return theme::tint(COVER_ACCENT,80);}
|
||||
inline lv_obj_t *cover_values[2]{};
|
||||
inline std::string cover_status_line(const Tile &t){return t.available()?tile_controls::cover_card_status(t):"Unavailable";}
|
||||
inline std::string cover_status_line(const Tile &t){return t.available()?tile_controls::cover_card_status(t):tr(txt::tile_unavailable);}
|
||||
inline void cover_slider_event(lv_event_t *e){
|
||||
auto *slider=lv_event_get_target_obj(e);auto code=lv_event_get_code(e);
|
||||
const bool tilt=(uintptr_t)lv_event_get_user_data(e)==1;
|
||||
@@ -1462,8 +1482,8 @@ inline void render_cover_detail(Tile &t,bool large,int width,int height,int pad)
|
||||
detail_card(pad,top,inner,box_h);
|
||||
struct Part{bool tilt;float value;const char *caption;};
|
||||
Part parts[2];int n=0;
|
||||
if(card.position)parts[n++]={false,t.position,"Position"};
|
||||
if(card.tilt)parts[n++]={true,t.extra().tilt,"Tilt"};
|
||||
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("--");};
|
||||
if(large){
|
||||
@@ -1550,7 +1570,7 @@ inline bool history_fits(const Tile &t){
|
||||
// Home Assistant's word for the state now: from this entity's history words, else the card's own. The history can
|
||||
// still be the previous card's while this one waits for its answer.
|
||||
inline std::string history_words(const Tile &t){
|
||||
if(!t.available())return "Unavailable";
|
||||
if(!t.available())return tr(txt::tile_unavailable);
|
||||
if(history.entity==t.entity)for(const auto &pair:history.words)if(pair.first==t.state)return pair.second;
|
||||
return detail_state(t);
|
||||
}
|
||||
@@ -1602,16 +1622,16 @@ inline void history_scrub(lv_event_t *e){
|
||||
const int i=history_view::part_at(fraction);
|
||||
const uint64_t span=h.end-h.start;
|
||||
const uint32_t begin=h.start+static_cast<uint32_t>(span*i/history_view::PARTS),finish=h.start+static_cast<uint32_t>(span*(i+1)/history_view::PARTS);
|
||||
label(c.value,h.has[i]?history_view::number(h.values[i],h.decimals,h.unit):"No data");
|
||||
label(c.value,h.has[i]?history_view::number(h.values[i],h.decimals,h.unit):std::string(tr(txt::history_no_data)));
|
||||
lv_obj_set_style_text_color(c.value,lv_color_hex(h.has[i]?theme::foreground(c.accent):theme::hex(theme::SUBTLE)),0);
|
||||
if(c.first)label(c.first,history_clock(begin,week)+" \u2013 "+history_clock(finish,false));
|
||||
if(c.second)label(c.second,!h.has[i]?"":h.hours==1?"average":"average of "+history_view::duration(finish-begin));
|
||||
if(c.second)label(c.second,!h.has[i]?std::string():h.hours==1?std::string(tr(txt::history_average)):fill(txt::history_average_of,"duration",history_view::duration(finish-begin)));
|
||||
}else{
|
||||
// A run reads the real times of its state, not the slots it covers: a door open for 5 minutes says 5 min.
|
||||
const int r=history_view::run_at(h.runs,h.slots,fraction);if(r<0)return;
|
||||
const auto &run=h.runs[r];
|
||||
const uint32_t begin=h.start+run.begin,finish=h.start+run.end;
|
||||
label(c.value,run.state<0||run.state>=static_cast<int>(h.states.size())?"No data":h.states[run.state].label);
|
||||
label(c.value,run.state<0||run.state>=static_cast<int>(h.states.size())?std::string(tr(txt::history_no_data)):h.states[run.state].label);
|
||||
if(c.first)label(c.first,history_clock(begin,week)+" \u2013 "+history_clock(finish,week&&finish-begin>=43200));
|
||||
if(c.second)label(c.second,history_view::duration(run.seconds));
|
||||
}
|
||||
@@ -1627,8 +1647,8 @@ inline void history_touch(int x,int y,int w,int h){
|
||||
inline void history_times(int x,int w,int y,const lv_font_t *font,bool large){
|
||||
const auto &h=history;
|
||||
const lv_font_t *bold=detail_font?detail_font:font;
|
||||
const int now_w=text_width("Now",bold),gap=large?10:6;
|
||||
detail_text(detail_root,"Now",x+w-now_w,y,now_w+2,bold,LV_TEXT_ALIGN_LEFT,theme::INK);
|
||||
const int now_w=text_width(tr(txt::history_now),bold),gap=large?10:6;
|
||||
detail_text(detail_root,tr(txt::history_now),x+w-now_w,y,now_w+2,bold,LV_TEXT_ALIGN_LEFT,theme::INK);
|
||||
int last=x-1000;
|
||||
for(uint32_t at:h.times){
|
||||
if(at<=h.start||at>=h.end)continue;
|
||||
@@ -1751,7 +1771,7 @@ inline void render_history_timeline(bool large,int card_x,int card_y,int card_w,
|
||||
// waiting for a command does not lock it.
|
||||
inline void history_ranges(int x,int y,int w,int h){
|
||||
static const uint32_t hours[]={1,24,168};
|
||||
static const char *const words[]={"1 hour","24 hours","1 week"};
|
||||
const char *const words[]={tr(txt::history_range_hour),tr(txt::history_range_day),tr(txt::history_range_week)};
|
||||
const lv_font_t *font=control_font?control_font:detail_font;
|
||||
auto *track=detail_shape(detail_root,x,y,w,h,theme::CARD,h/2);
|
||||
lv_obj_set_style_border_width(track,1,0);lv_obj_set_style_border_color(track,theme::color(theme::LINE),0);
|
||||
@@ -1789,7 +1809,7 @@ inline void render_history_detail(const Tile &t,bool large,int width,int height,
|
||||
if(line){
|
||||
int decimals=0;const auto dot=t.state.find('.');if(dot!=std::string::npos)decimals=static_cast<int>(std::min<size_t>(4,t.state.size()-dot-1));
|
||||
if(c.ready)decimals=h.decimals;
|
||||
c.value_text=!t.available()?"Unavailable":numeric?history_view::number(current,decimals,t.unit):t.state;
|
||||
c.value_text=!t.available()?std::string(tr(txt::tile_unavailable)):numeric?history_view::number(current,decimals,t.unit):t.state;
|
||||
if(c.ready){
|
||||
// The value now counts too: it can lie beyond the history (the running hour is not in the statistics yet).
|
||||
if(h.has_high){c.high=h.high;c.high_at=h.high_at;}
|
||||
@@ -1797,13 +1817,13 @@ inline void render_history_detail(const Tile &t,bool large,int width,int height,
|
||||
if(std::isfinite(current)&&std::isfinite(c.high)&¤t>c.high){c.high=current;c.high_at=h.end;}
|
||||
if(std::isfinite(current)&&std::isfinite(c.low)&¤t<c.low){c.low=current;c.low_at=h.end;}
|
||||
}
|
||||
if(std::isfinite(c.high))c.first_text="High "+history_view::number(c.high,h.decimals,h.unit)+" \u00B7 "+history_clock(c.high_at,h.hours==168);
|
||||
if(std::isfinite(c.low))c.second_text="Low "+history_view::number(c.low,h.decimals,h.unit)+" \u00B7 "+history_clock(c.low_at,h.hours==168);
|
||||
if(std::isfinite(c.high))c.first_text=fill(txt::history_high,"value",history_view::number(c.high,h.decimals,h.unit))+" \u00B7 "+history_clock(c.high_at,h.hours==168);
|
||||
if(std::isfinite(c.low))c.second_text=fill(txt::history_low,"value",history_view::number(c.low,h.decimals,h.unit))+" \u00B7 "+history_clock(c.low_at,h.hours==168);
|
||||
}else{
|
||||
c.value_text=history_words(t);
|
||||
if(c.ready&&h.active>=0){
|
||||
c.first_text=h.states[h.active].label+" \u00B7 "+history_view::duration(h.states[h.active].seconds);
|
||||
c.second_text=h.began==1?"once":h.began?std::to_string(h.began)+" times":"";
|
||||
c.second_text=h.began?plural(txt::history_times,h.began):"";
|
||||
}
|
||||
}
|
||||
const int row=large?84:50,row_h=lv_font_get_line_height(big),text_h=lv_font_get_line_height(text);
|
||||
@@ -1832,7 +1852,7 @@ inline void render_history_detail(const Tile &t,bool large,int width,int height,
|
||||
}
|
||||
// The value column fits the widest text a finger can show there, so a readout never runs into the texts beside it.
|
||||
int widest=text_width(c.value_text,big);
|
||||
if(c.ready)widest=std::max(widest,text_width("No data",big));
|
||||
if(c.ready)widest=std::max(widest,text_width(tr(txt::history_no_data),big));
|
||||
if(c.ready&&line){for(int i=0;i<history_view::PARTS;++i)if(h.has[i])widest=std::max(widest,text_width(history_view::number(h.values[i],h.decimals,h.unit),big));}
|
||||
else if(c.ready){for(const auto &s:h.states)widest=std::max(widest,text_width(s.label,big));}
|
||||
const int value_x=pad+(large?8:4),value_w=std::min(widest+6,(width-2*pad)*11/20);
|
||||
@@ -1853,7 +1873,7 @@ inline void render_history_detail(const Tile &t,bool large,int width,int height,
|
||||
detail_card(pad,top,width-2*pad,card_h);
|
||||
const bool empty=c.ready&&(line?std::none_of(h.has,h.has+history_view::PARTS,[](bool v){return v;}):h.states.empty());
|
||||
if(!c.ready||empty){
|
||||
c.status=detail_text(detail_root,!c.ready?"Loading history...":"No history in this period",pad,top+(card_h-text_h)/2,width-2*pad,text,LV_TEXT_ALIGN_CENTER,theme::SUBTLE);
|
||||
c.status=detail_text(detail_root,tr(!c.ready?txt::history_loading:txt::history_empty),pad,top+(card_h-text_h)/2,width-2*pad,text,LV_TEXT_ALIGN_CENTER,theme::SUBTLE);
|
||||
}else if(line){
|
||||
render_history_line(large,pad,top,width-2*pad,card_h,small,current);
|
||||
}else{
|
||||
@@ -1889,7 +1909,7 @@ inline void media_action(Tile &t,int cmd){
|
||||
// An off or standby player shows one key: power, when the player can be turned on from here.
|
||||
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 "Muted";
|
||||
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))+" %";
|
||||
}
|
||||
@@ -2069,12 +2089,12 @@ inline void show_detail(unsigned index){
|
||||
}else if(d=="weather"){
|
||||
render_weather_detail(t,large,width,height,pad);
|
||||
}else if(d=="timer"){
|
||||
detail_label(detail_root,t.state=="active"?"Running":t.state=="paused"?"Paused":"Stopped",pad,top,width-2*pad);
|
||||
detail_button(t.state=="active"?"Pause":"Start",pad,top+(large?50:30),cw,bh,40);
|
||||
detail_button("Cancel",pad+cw+gap,top+(large?50:30),cw,bh,41);
|
||||
detail_label(detail_root,tr(t.state=="active"?txt::timer_running:t.state=="paused"?txt::timer_paused:txt::timer_stopped),pad,top,width-2*pad);
|
||||
detail_button(tr(t.state=="active"?txt::timer_pause:txt::timer_start),pad,top+(large?50:30),cw,bh,40);
|
||||
detail_button(tr(txt::timer_cancel),pad+cw+gap,top+(large?50:30),cw,bh,41);
|
||||
}else if(d=="sun"){
|
||||
detail_label(detail_root,"Sunrise "+t.extra().sunrise,pad,top,width-2*pad);
|
||||
detail_label(detail_root,"Sunset "+t.extra().sunset,pad,top+lv_font_get_line_height(detail_font)+(large?10:4),width-2*pad);
|
||||
detail_label(detail_root,fill(txt::sun_sunrise,"time",t.extra().sunrise),pad,top,width-2*pad);
|
||||
detail_label(detail_root,fill(txt::sun_sunset,"time",t.extra().sunset),pad,top+lv_font_get_line_height(detail_font)+(large?10:4),width-2*pad);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2108,20 +2128,20 @@ inline const char *weather_icon(const std::string &condition) {
|
||||
return "\U000F0595";
|
||||
}
|
||||
inline const char *weather_text(const std::string &condition) {
|
||||
if (condition == "sunny") return "Sunny";
|
||||
if (condition == "clear-night") return "Clear, night";
|
||||
if (condition == "cloudy") return "Cloudy";
|
||||
if (condition == "partlycloudy") return "Partly cloudy";
|
||||
if (condition == "rainy") return "Rainy";
|
||||
if (condition == "pouring") return "Pouring";
|
||||
if (condition == "snowy") return "Snowy";
|
||||
if (condition == "snowy-rainy") return "Snowy, rainy";
|
||||
if (condition == "fog") return "Fog";
|
||||
if (condition == "hail") return "Hail";
|
||||
if (condition == "lightning") return "Lightning";
|
||||
if (condition == "lightning-rainy") return "Lightning, rainy";
|
||||
if (condition == "windy" || condition == "windy-variant") return "Windy";
|
||||
if (condition == "exceptional") return "Exceptional";
|
||||
if (condition == "sunny") return tr(txt::ha_weather_sunny);
|
||||
if (condition == "clear-night") return tr(txt::ha_weather_clear_night);
|
||||
if (condition == "cloudy") return tr(txt::ha_weather_cloudy);
|
||||
if (condition == "partlycloudy") return tr(txt::ha_weather_partlycloudy);
|
||||
if (condition == "rainy") return tr(txt::ha_weather_rainy);
|
||||
if (condition == "pouring") return tr(txt::ha_weather_pouring);
|
||||
if (condition == "snowy") return tr(txt::ha_weather_snowy);
|
||||
if (condition == "snowy-rainy") return tr(txt::ha_weather_snowy_rainy);
|
||||
if (condition == "fog") return tr(txt::ha_weather_fog);
|
||||
if (condition == "hail") return tr(txt::ha_weather_hail);
|
||||
if (condition == "lightning") return tr(txt::ha_weather_lightning);
|
||||
if (condition == "lightning-rainy") return tr(txt::ha_weather_lightning_rainy);
|
||||
if (condition == "windy" || condition == "windy-variant") return tr(txt::ha_weather_windy);
|
||||
if (condition == "exceptional") return tr(txt::ha_weather_exceptional);
|
||||
return condition.c_str();
|
||||
}
|
||||
inline const char *icon_for(const Tile &tile) {
|
||||
@@ -2150,36 +2170,38 @@ inline std::string countdown(uint32_t seconds) {
|
||||
else snprintf(b, sizeof(b), "%u:%02u", seconds / 60, seconds % 60);
|
||||
return b;
|
||||
}
|
||||
// "Last 14:32" today, "Yesterday 14:32", else "Last 13 Sep"; scripts and scenes have no useful on/off.
|
||||
// "Last 14:32" today, "Yesterday 14:32", else "Last 13 Sep", in the screen's language; scripts and scenes have no
|
||||
// useful on/off.
|
||||
inline std::string month_short(const esphome::ESPTime &now);
|
||||
inline std::string last_run_text(uint32_t epoch) {
|
||||
if (!epoch) return "Never run";
|
||||
if (!epoch) return tr(txt::script_never_run);
|
||||
auto when = esphome::ESPTime::from_epoch_local(epoch);
|
||||
auto now = now_time ? now_time() : esphome::ESPTime{};
|
||||
if (!when.is_valid()) return "Never run";
|
||||
if (!when.is_valid()) return tr(txt::script_never_run);
|
||||
char clock[8]; snprintf(clock, sizeof(clock), "%02d:%02d", when.hour, when.minute);
|
||||
if (now.is_valid() && now.year == when.year && now.day_of_year == when.day_of_year) return std::string("Last ") + clock;
|
||||
if (now.is_valid() && now.year == when.year && now.day_of_year == when.day_of_year + 1) return std::string("Yesterday ") + clock;
|
||||
return "Last " + std::to_string(when.day_of_month) + " " + month_short(when);
|
||||
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));
|
||||
return fill(txt::script_last_date, "date", date);
|
||||
}
|
||||
inline std::string timer_text(const Tile &t) {
|
||||
const Extra &x = t.extra();
|
||||
if (t.state == "active") return countdown(timer_left(x.timer_end, now_epoch(), x.duration));
|
||||
if (t.state == "paused") return "Paused " + countdown(duration_seconds(x.remaining));
|
||||
return x.duration.empty() ? "Off" : countdown(duration_seconds(x.duration));
|
||||
if (t.state == "paused") return fill(txt::timer_paused_left, "time", countdown(duration_seconds(x.remaining)));
|
||||
return x.duration.empty() ? std::string(tr(txt::ha_off)) : countdown(duration_seconds(x.duration));
|
||||
}
|
||||
inline std::string weekday_text(const esphome::ESPTime &now) {
|
||||
static const char *days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
|
||||
return now.is_valid() && now.day_of_week >= 1 && now.day_of_week <= 7 ? days[now.day_of_week - 1] : "";
|
||||
return now.is_valid() && now.day_of_week >= 1 && now.day_of_week <= 7 ? tr(txt::date_weekdays + now.day_of_week - 1) : "";
|
||||
}
|
||||
inline std::string month_short(const esphome::ESPTime &now) {
|
||||
static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
||||
return now.is_valid() && now.month >= 1 && now.month <= 12 ? months[now.month - 1] : "";
|
||||
return now.is_valid() && now.month >= 1 && now.month <= 12 ? tr(txt::date_months_short + now.month - 1) : "";
|
||||
}
|
||||
// "Monday 14 September" in English, "maandag 14 september" in Dutch (screen.date.full).
|
||||
inline std::string date_text(const esphome::ESPTime &now) {
|
||||
static const char *months[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
|
||||
if (!now.is_valid() || now.day_of_week < 1 || now.day_of_week > 7 || now.month < 1 || now.month > 12) return "";
|
||||
return weekday_text(now) + " " + std::to_string(now.day_of_month) + " " + months[now.month - 1];
|
||||
std::string text = fill(txt::date_full, "weekday", weekday_text(now));
|
||||
text = fill(text, "day", std::to_string(now.day_of_month));
|
||||
return fill(text, "month", tr(txt::date_months + now.month - 1));
|
||||
}
|
||||
// Where the finger is, or where it let go, in the screen's coordinates; false without a finger (a test's event).
|
||||
inline bool finger_at(lv_point_t &point) {
|
||||
@@ -2615,9 +2637,9 @@ inline void render_sunpath(Widgets &w,const Tile &t,bool large,int width,int hei
|
||||
const lv_font_t *title_font=lv_obj_get_style_text_font(w.title,LV_PART_MAIN);
|
||||
int title_h=lv_font_get_line_height(title_font),text_h=lv_font_get_line_height(w.value_font);
|
||||
int horizon=height-text_h-(large?4:2),top=title_h+(large?4:2),x0=large?14:8,x1=width-x0;
|
||||
part_label(w,0,title_font,0,0,width,LV_TEXT_ALIGN_LEFT,t.name.empty()?"Sun":t.name);
|
||||
part_label(w,1,w.value_font,0,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_LEFT,"rise "+t.extra().sunrise);
|
||||
part_label(w,2,w.value_font,width/2,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_RIGHT,"set "+t.extra().sunset);
|
||||
part_label(w,0,title_font,0,0,width,LV_TEXT_ALIGN_LEFT,t.name.empty()?std::string(tr(txt::sun_name)):t.name);
|
||||
part_label(w,1,w.value_font,0,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_LEFT,fill(txt::sun_rise,"time",t.extra().sunrise));
|
||||
part_label(w,2,w.value_font,width/2,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_RIGHT,fill(txt::sun_set,"time",t.extra().sunset));
|
||||
auto now=now_time?now_time():esphome::ESPTime{};
|
||||
int rise=minutes_of(t.extra().sunrise),set=minutes_of(t.extra().sunset),minute=now.is_valid()?now.hour*60+now.minute:-1;
|
||||
bool day=t.state=="above_horizon";float fraction=0.5f;
|
||||
@@ -3068,7 +3090,7 @@ inline void render_full(Widgets &w,const Tile &t,bool custom,bool clock,bool sun
|
||||
// The large value font carries digits, the degree sign and the percent sign; the clock font only digits and a colon.
|
||||
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)){char b[32];snprintf(b,sizeof(b),"%.1f°",t.current);middle=b;}
|
||||
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=="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);
|
||||
@@ -3092,34 +3114,34 @@ inline void render_slot(size_t slot) {
|
||||
std::string unit=watch?t.unit:"";
|
||||
std::string value = t.state;
|
||||
// Nothing in Home Assistant stands behind the settings card, so it says the same with the link down.
|
||||
if (t.is_settings()) value = "Tap to open";
|
||||
else if (t.is_page()) value = "Page " + std::to_string(t.page_target());
|
||||
else if (!fresh() || !t.available()) value = "Unavailable";
|
||||
else if (t.refused_at && esphome::millis() - t.refused_at < 4000) value = "Refused";
|
||||
if (t.is_settings()) value = tr(txt::tile_tap_to_open);
|
||||
else if (t.is_page()) value = fill(txt::tile_page, "n", t.page_target());
|
||||
else if (!fresh() || !t.available()) value = tr(txt::tile_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))) + " %";
|
||||
// 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";
|
||||
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) + "°"; }
|
||||
else if (d == "climate" && std::isfinite(t.target)) value = screen_text::decimal(t.target, 1) + "°";
|
||||
else if (d == "person") value = t.state=="home"?tr(txt::ha_person_home):t.state=="not_home"?tr(txt::ha_person_not_home):t.state;
|
||||
else if (d == "sun") value = !t.extra().sunrise.empty() && !t.extra().sunset.empty() ? t.extra().sunrise+" - "+t.extra().sunset : tr(t.state=="above_horizon"?txt::ha_sun_above_horizon:txt::ha_sun_below_horizon);
|
||||
else if (d == "timer") value = timer_text(t);
|
||||
else if (d == "script" || d == "scene" || d == "button" || d == "input_button") value = t.state == "on" ? "Running..." : last_run_text(t.last_run);
|
||||
else if (d == "camera") value = t.state == "streaming" ? "Live" : t.state == "recording" ? "Recording" : "Tap to view";
|
||||
else if (d == "image") value = t.last_run ? "Tap to view" : "No image yet";
|
||||
else if (d == "script" || d == "scene" || d == "button" || d == "input_button") value = t.state == "on" ? std::string(tr(txt::script_running)) : last_run_text(t.last_run);
|
||||
else if (d == "camera") value = tr(t.state == "streaming" ? txt::camera_live : t.state == "recording" ? txt::camera_recording : txt::camera_tap_to_view);
|
||||
else if (d == "image") value = tr(t.last_run ? txt::camera_tap_to_view : txt::camera_no_image_yet);
|
||||
else if (d == "binary_sensor" && (value == "on" || value == "off")) value = tile_controls::binary_state_text(t.device_class, value == "on");
|
||||
else if (value == "on") value = "On";
|
||||
else if (value == "off") value = "Off";
|
||||
else if (value == "cleaning") value = "Cleaning";
|
||||
else if (value == "docked") value = "Docked";
|
||||
else if (value == "on") value = tr(txt::ha_on);
|
||||
else if (value == "off") value = tr(txt::ha_off);
|
||||
else if (value == "cleaning") value = tr(txt::ha_vacuum_cleaning);
|
||||
else if (value == "docked") value = tr(txt::ha_vacuum_docked);
|
||||
// Home Assistant's word where the screen has none of its own (firmware 0.2.58+): a cover says Open, a washer Rinsing.
|
||||
else if (!t.extra().state_word.empty()) value = t.extra().state_word;
|
||||
// A player's state in the screen's own words where Home Assistant sent none (firmware 0.2.64+).
|
||||
else if (d == "media_player") value = tile_controls::media_state_text(t.state);
|
||||
else if (!t.unit.empty() && !watch) value += " " + t.unit;
|
||||
bool pending=t.loading(esphome::millis());
|
||||
if(d=="weather" && std::isfinite(t.current)) {char b[32];snprintf(b,sizeof(b),"%.1f %s",t.current,t.unit.c_str());value=b;if(watch){snprintf(b,sizeof(b),"%.1f",t.current);value=b;}}
|
||||
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)+"%";
|
||||
// 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();
|
||||
@@ -3338,9 +3360,9 @@ inline void boot_status(lv_obj_t *page, const char *text) {
|
||||
inline void render(lv_obj_t *room) {
|
||||
if (!enabled) return;
|
||||
room_label=room; swipe_profile::Lap lap;
|
||||
if (!model.configured) boot_status(lv_obj_get_parent(room), !ha_connected() ? "Connecting to Home Assistant" : "Waiting for ESP Screens");
|
||||
if (!model.configured) boot_status(lv_obj_get_parent(room), tr(!ha_connected() ? txt::status_connecting : txt::status_waiting));
|
||||
else if (boot_panel) { lv_obj_delete(boot_panel); boot_panel = boot_text = boot_spinner = nullptr; }
|
||||
label(room, !model.configured ? "" : !model.ready() ? "Loading tiles..." : !ha_connected() ? "HA not connected" : !feed_alive() ? "ESP Screens not active" : model.title);
|
||||
label(room, !model.configured ? std::string() : !model.ready() ? tr(txt::status_loading_tiles) : !ha_connected() ? tr(txt::status_ha_not_connected) : !feed_alive() ? tr(txt::status_manager_not_active) : model.title);
|
||||
render_header();
|
||||
lap(swipe_profile::HEADER);
|
||||
bool all=dirty_all || (!dirty_tiles && !dirty_header);
|
||||
@@ -3856,15 +3878,15 @@ inline void tick() {
|
||||
const uint32_t now=esphome::millis();
|
||||
if(history_fits(t))refresh_detail(detail_index);
|
||||
else if(now-history_asked_at>30000)history_request(t.entity,history_hours);
|
||||
else if(now-history_asked_at>8000)label(history_chart.status,"No history available");
|
||||
else if(now-history_asked_at>8000)label(history_chart.status,tr(txt::history_unavailable));
|
||||
}
|
||||
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?(t.confirmed?"Confirmed by Home Assistant":"Command sent..."):t.domain()=="cover"?cover_status_line(t):detail_state(t);
|
||||
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:""));
|
||||
// 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"){
|
||||
status=waiting && !t.confirmed?"Command sent...":detail_state(t);
|
||||
status=waiting && !t.confirmed?std::string(tr(txt::tile_command_sent)):detail_state(t);
|
||||
if(status!=lv_label_get_text(detail_badge_status))refresh_detail(detail_index);
|
||||
}else if(detail_badge_status)label(detail_badge_status,status);
|
||||
if(detail_switch && !lv_obj_has_state(detail_switch,LV_STATE_PRESSED)){
|
||||
@@ -4261,7 +4283,7 @@ inline void camera_answer(const std::string &view, const std::string &entity, co
|
||||
if (!camera_root || camera.entity != entity) return;
|
||||
camera.link(url);
|
||||
if (url.empty()) {
|
||||
if (!camera.shown) camera_note_text("No image from this camera");
|
||||
if (!camera.shown) camera_note_text(tr(txt::camera_no_image));
|
||||
return;
|
||||
}
|
||||
// Loaded now rather than on the next tick (firmware 0.2.73+), as an alert's image is.
|
||||
@@ -4364,6 +4386,6 @@ inline void camera_failed(bool thumb) {
|
||||
}
|
||||
if (!camera.loading) return;
|
||||
camera.finish(esphome::millis(), false);
|
||||
if (!camera.shown) camera_note_text("No image from this camera");
|
||||
if (!camera.shown) camera_note_text(tr(txt::camera_no_image));
|
||||
}
|
||||
} // namespace runtime_tiles
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
// The texts a screen shows, in the one language its firmware was built for (app 0.2.90). Every text has a key in the
|
||||
// `screen` section of screen_manager/translations/<code>.json; screen_text_keys.h numbers the keys, and the build
|
||||
// writes that language's TABLE into main.cpp (components/smart_display/screen_text_gen.py). A lookup is an index into
|
||||
// flash: no text is copied, searched for or kept in RAM, and a screen carries no language but its own.
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include "screen_text_keys.h"
|
||||
|
||||
namespace screen_text {
|
||||
|
||||
extern const char *const TABLE[]; // this build's language, one text per key
|
||||
extern const char *const LANGUAGE; // its code, such as "nl"; the "Screen language" sensor reports it
|
||||
int plural_index(int n); // which form of a plural text fits n in this language
|
||||
|
||||
inline const char *tr(uint16_t id) { return id < KEY_COUNT ? TABLE[id] : ""; }
|
||||
|
||||
// `text` with every {name} replaced by `value`. Placeholders are named, so a language puts them where its sentence
|
||||
// needs them; a translation is never used as a printf format.
|
||||
inline std::string fill(const std::string &text, const char *name, const std::string &value) {
|
||||
std::string out = text, mark = std::string("{") + name + "}";
|
||||
for (size_t at = out.find(mark); at != std::string::npos; at = out.find(mark, at + value.size()))
|
||||
out.replace(at, mark.size(), value);
|
||||
return out;
|
||||
}
|
||||
inline std::string fill(uint16_t id, const char *name, const std::string &value) { return fill(std::string(tr(id)), name, value); }
|
||||
inline std::string fill(uint16_t id, const char *name, int value) { return fill(id, name, std::to_string(value)); }
|
||||
|
||||
// The form of a plural text ("1 hour ago | {n} hours ago") for n, with {n} filled in. The forms stand in the order
|
||||
// of the language's plural rule; a text with fewer forms than the rule uses its last one.
|
||||
inline std::string plural(uint16_t id, int n) {
|
||||
const char *text = tr(id);
|
||||
int wanted = plural_index(n);
|
||||
const char *start = text;
|
||||
for (int form = 0; form < wanted; ++form) {
|
||||
const char *bar = strchr(start, '|');
|
||||
if (!bar) break;
|
||||
start = bar + 1;
|
||||
}
|
||||
const char *end = strchr(start, '|');
|
||||
std::string form = end ? std::string(start, end - start) : std::string(start);
|
||||
size_t first = form.find_first_not_of(' '), last = form.find_last_not_of(' ');
|
||||
form = first == std::string::npos ? std::string() : form.substr(first, last - first + 1);
|
||||
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;
|
||||
inline char decimal_mark() {
|
||||
if (number_style == 1) return '.';
|
||||
if (number_style == 2 || number_style == 3) return ',';
|
||||
const char *mark = tr(txt::number_decimal);
|
||||
return mark[0] ? mark[0] : '.';
|
||||
}
|
||||
// The separator between thousands.
|
||||
inline const char *group_mark() {
|
||||
if (number_style == 1) return ",";
|
||||
if (number_style == 2) return ".";
|
||||
if (number_style == 3) return " ";
|
||||
return tr(txt::number_group);
|
||||
}
|
||||
// "point", "comma", "space" or "auto" as its number_style; -1 for anything else.
|
||||
inline int number_style_of(const std::string &name) {
|
||||
if (name == "auto") return 0;
|
||||
if (name == "point") return 1;
|
||||
if (name == "comma") return 2;
|
||||
if (name == "space") return 3;
|
||||
return -1;
|
||||
}
|
||||
// A number with `decimals` places, written the way this language writes it ("21,5" in Dutch).
|
||||
inline std::string decimal(float value, int decimals) {
|
||||
char buffer[24];
|
||||
snprintf(buffer, sizeof(buffer), "%.*f", decimals, (double) value);
|
||||
std::string text = buffer;
|
||||
if (decimal_mark() != '.') {
|
||||
size_t dot = text.find('.');
|
||||
if (dot != std::string::npos) text[dot] = decimal_mark();
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
} // namespace screen_text
|
||||
@@ -0,0 +1,172 @@
|
||||
"""The texts a screen shows, in the language it is built for (app 0.2.90).
|
||||
|
||||
Every text lives in screen_manager/translations/<code>.json; its `screen` section is what the firmware draws. A build
|
||||
takes one language: this module turns that section into one C++ table in the build's main.cpp, so a screen carries a
|
||||
single language and looks a text up by its index (screen_text.h). The keys are the same for every language and come
|
||||
from en.json in document order; tools/i18n.py writes them to screen_text_keys.h, and a static_assert in the build
|
||||
checks that header against the JSON it is built from. A key a language lacks takes the English text.
|
||||
|
||||
Plain Python on purpose, without ESPHome: __init__.py imports it for the build, tools/i18n.py and the tests use it too.
|
||||
The ESP Screens app has its own reader (screen_manager/app/i18n.py), as the app's image holds no components folder.
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# The repository root: components/smart_display/ sits two folders down, in a checkout and in the clone ESPHome makes
|
||||
# of the repository for an external component (git clone --depth=1 of the whole repository).
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
TRANSLATIONS = ROOT / 'screen_manager' / 'translations'
|
||||
LANGUAGE_CODE = re.compile(r'[a-z]{2,3}(-[A-Za-z0-9]{2,8})?')
|
||||
|
||||
# How a language picks the form of a plural text ("1 hour ago | {n} hours ago"): the index of the form for n, as C++.
|
||||
# The editor has the same rules (web/src/i18n.ts); docs/TRANSLATING.md lists which languages use which.
|
||||
PLURAL_RULES = {
|
||||
'one_other': 'return n == 1 ? 0 : 1;',
|
||||
'one_upto_1': 'return (n < 0 ? -n : n) <= 1 ? 0 : 1;',
|
||||
'slavic_pl': 'if (n == 1) return 0; int t = n % 10, h = n % 100; '
|
||||
'return t >= 2 && t <= 4 && (h < 12 || h > 14) ? 1 : 2;',
|
||||
'east_slavic': 'int t = n % 10, h = n % 100; if (t == 1 && h != 11) return 0; '
|
||||
'return t >= 2 && t <= 4 && (h < 12 || h > 14) ? 1 : 2;',
|
||||
'none': 'return 0;',
|
||||
}
|
||||
|
||||
|
||||
def load(code, folder=TRANSLATIONS):
|
||||
"""A language file as a dict; None when there is none."""
|
||||
path = Path(folder) / f'{code}.json'
|
||||
if not LANGUAGE_CODE.fullmatch(code or '') or not path.is_file():
|
||||
return None
|
||||
return json.loads(path.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def resolve(code, folder=TRANSLATIONS):
|
||||
"""The language a build takes for `code`: the file itself, else its base language (pt-BR -> pt), else English."""
|
||||
for candidate in (code, (code or '').split('-')[0]):
|
||||
if candidate and load(candidate, folder) is not None:
|
||||
return candidate
|
||||
return 'en'
|
||||
|
||||
|
||||
def flatten(node, prefix=''):
|
||||
"""(key, text) pairs in document order; a list's items get .0, .1, ... ."""
|
||||
if isinstance(node, dict):
|
||||
items = node.items()
|
||||
elif isinstance(node, list):
|
||||
items = ((str(index), value) for index, value in enumerate(node))
|
||||
else:
|
||||
return [(prefix, node)]
|
||||
pairs = []
|
||||
for key, value in items:
|
||||
pairs.extend(flatten(value, f'{prefix}.{key}' if prefix else key))
|
||||
return pairs
|
||||
|
||||
|
||||
def screen_pairs(data):
|
||||
"""The `screen` section as (key, text) pairs, the key without the section."""
|
||||
return [(key, value) for key, value in flatten((data or {}).get('screen', {})) if isinstance(value, str)]
|
||||
|
||||
|
||||
def identifier(key):
|
||||
"""The C++ name of a key: settings.dark_mode -> settings_dark_mode."""
|
||||
return re.sub(r'[^A-Za-z0-9]+', '_', key).strip('_')
|
||||
|
||||
|
||||
def keys_hash(keys):
|
||||
"""FNV-1a over the keys, so a build notices a screen_text_keys.h that no longer matches the JSON."""
|
||||
value = 0x811C9DC5
|
||||
for byte in '\n'.join(keys).encode('utf-8'):
|
||||
value = ((value ^ byte) * 0x01000193) & 0xFFFFFFFF
|
||||
return value
|
||||
|
||||
|
||||
def cpp_string(text):
|
||||
"""A C++ string literal of UTF-8 text; every byte outside printable ASCII as a three-digit octal escape."""
|
||||
out = []
|
||||
for byte in text.encode('utf-8'):
|
||||
char = chr(byte)
|
||||
if char in '"\\':
|
||||
out.append('\\' + char)
|
||||
elif 32 <= byte < 127:
|
||||
out.append(char)
|
||||
else:
|
||||
out.append(f'\\{byte:03o}')
|
||||
return '"' + ''.join(out) + '"'
|
||||
|
||||
|
||||
def table(code, folder=TRANSLATIONS):
|
||||
"""(language, keys, texts, plural rule) of a build: every English key, in its language where it has one."""
|
||||
english = load('en', folder)
|
||||
language = resolve(code, folder)
|
||||
own = dict(screen_pairs(load(language, folder))) if language != 'en' else {}
|
||||
keys, texts = [], []
|
||||
for key, text in screen_pairs(english):
|
||||
keys.append(key)
|
||||
translated = own.get(key)
|
||||
texts.append(translated if isinstance(translated, str) and translated.strip() else text)
|
||||
meta = (load(language, folder) or {}).get('_meta', {})
|
||||
rule = meta.get('plural') if meta.get('plural') in PLURAL_RULES else 'one_other'
|
||||
return language, keys, texts, rule
|
||||
|
||||
|
||||
def arrays(keys):
|
||||
"""{list key: (index of its first item, length)} for the lists among the keys (date.months_short.0 ...)."""
|
||||
found = {}
|
||||
for index, key in enumerate(keys):
|
||||
head, _, last = key.rpartition('.')
|
||||
if head and last.isdigit():
|
||||
start, length = found.get(head, (index, 0))
|
||||
found[head] = (start, length + 1)
|
||||
return found
|
||||
|
||||
|
||||
def keys_header(keys):
|
||||
"""screen_text_keys.h: the index of every key, and the hash of them all."""
|
||||
lines = [
|
||||
'// GENERATED by tools/i18n.py from screen_manager/translations/en.json; run `python3 tools/i18n.py header`',
|
||||
'// after changing the keys of the `screen` section. The index of every text a screen shows in its language\'s',
|
||||
'// table (screen_text.h); a list such as the month names is its first index plus a count.',
|
||||
'#pragma once',
|
||||
'#include <cstdint>',
|
||||
'',
|
||||
'namespace screen_text {',
|
||||
'namespace txt {',
|
||||
]
|
||||
lists = arrays(keys)
|
||||
listed = {index for start, length in lists.values() for index in range(start, start + length)}
|
||||
for index, key in enumerate(keys):
|
||||
if index not in listed:
|
||||
lines.append(f'constexpr uint16_t {identifier(key)} = {index};')
|
||||
for key, (start, length) in lists.items():
|
||||
lines.append(f'constexpr uint16_t {identifier(key)} = {start};')
|
||||
lines.append(f'constexpr uint16_t {identifier(key)}_count = {length};')
|
||||
lines += [
|
||||
'} // namespace txt',
|
||||
f'constexpr uint16_t KEY_COUNT = {len(keys)};',
|
||||
f'constexpr uint32_t KEYS_HASH = 0x{keys_hash(keys):08X}u;',
|
||||
'} // namespace screen_text',
|
||||
'',
|
||||
]
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
def definitions(code, folder=TRANSLATIONS):
|
||||
"""The C++ a build adds to main.cpp: its language's table and plural rule, and the check against the keys header."""
|
||||
language, keys, texts, rule = table(code, folder)
|
||||
rows = ',\n'.join(f' {cpp_string(text)}' for text in texts)
|
||||
return language, (
|
||||
f'static_assert(screen_text::KEYS_HASH == 0x{keys_hash(keys):08X}u && screen_text::KEY_COUNT == {len(keys)},\n'
|
||||
' "screen_text_keys.h does not match screen_manager/translations/en.json: run tools/i18n.py header");\n'
|
||||
f'const char *const screen_text::TABLE[] = {{\n{rows}\n}};\n'
|
||||
f'const char *const screen_text::LANGUAGE = {cpp_string(language)};\n'
|
||||
f'int screen_text::plural_index(int n) {{ {PLURAL_RULES[rule]} }}\n'
|
||||
)
|
||||
|
||||
|
||||
def english_header(folder=TRANSLATIONS):
|
||||
"""tests/screen_text_en.h: the English table for the C++ tests, which build without ESPHome."""
|
||||
_, text = definitions('en', folder)
|
||||
return ('// GENERATED by tools/i18n.py (`python3 tools/i18n.py header`): the English texts for the C++ tests, which\n'
|
||||
'// build a header without ESPHome and so without the table a screen\'s build writes into main.cpp.\n'
|
||||
'#pragma once\n'
|
||||
'#include "components/smart_display/screen_text.h"\n\n' + text)
|
||||
@@ -0,0 +1,279 @@
|
||||
// GENERATED by tools/i18n.py from screen_manager/translations/en.json; run `python3 tools/i18n.py header`
|
||||
// after changing the keys of the `screen` section. The index of every text a screen shows in its language's
|
||||
// table (screen_text.h); a list such as the month names is its first index plus a count.
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace screen_text {
|
||||
namespace txt {
|
||||
constexpr uint16_t number_decimal = 0;
|
||||
constexpr uint16_t number_group = 1;
|
||||
constexpr uint16_t time_just_now = 2;
|
||||
constexpr uint16_t time_minutes_ago = 3;
|
||||
constexpr uint16_t time_hours_ago = 4;
|
||||
constexpr uint16_t time_yesterday = 5;
|
||||
constexpr uint16_t time_days_ago = 6;
|
||||
constexpr uint16_t time_weeks_ago = 7;
|
||||
constexpr uint16_t time_months_ago = 8;
|
||||
constexpr uint16_t time_years_ago = 9;
|
||||
constexpr uint16_t time_in_minutes = 10;
|
||||
constexpr uint16_t time_in_hours = 11;
|
||||
constexpr uint16_t time_tomorrow = 12;
|
||||
constexpr uint16_t time_in_days = 13;
|
||||
constexpr uint16_t time_am = 14;
|
||||
constexpr uint16_t time_pm = 15;
|
||||
constexpr uint16_t date_top_bar = 61;
|
||||
constexpr uint16_t date_full = 62;
|
||||
constexpr uint16_t date_day_month = 63;
|
||||
constexpr uint16_t date_weekday_time = 64;
|
||||
constexpr uint16_t duration_seconds = 65;
|
||||
constexpr uint16_t duration_minutes = 66;
|
||||
constexpr uint16_t duration_hours = 67;
|
||||
constexpr uint16_t duration_hours_minutes = 68;
|
||||
constexpr uint16_t duration_days = 69;
|
||||
constexpr uint16_t duration_days_hours = 70;
|
||||
constexpr uint16_t status_connecting = 71;
|
||||
constexpr uint16_t status_waiting = 72;
|
||||
constexpr uint16_t status_loading_tiles = 73;
|
||||
constexpr uint16_t status_ha_not_connected = 74;
|
||||
constexpr uint16_t status_manager_not_active = 75;
|
||||
constexpr uint16_t status_choose_tiles = 76;
|
||||
constexpr uint16_t status_home = 77;
|
||||
constexpr uint16_t status_no_network = 78;
|
||||
constexpr uint16_t settings_title = 79;
|
||||
constexpr uint16_t settings_brightness = 80;
|
||||
constexpr uint16_t settings_night = 81;
|
||||
constexpr uint16_t settings_screen = 82;
|
||||
constexpr uint16_t settings_this_screen = 83;
|
||||
constexpr uint16_t settings_dark_mode = 84;
|
||||
constexpr uint16_t settings_auto_standby = 85;
|
||||
constexpr uint16_t settings_standby_after = 86;
|
||||
constexpr uint16_t settings_standby_brightness = 87;
|
||||
constexpr uint16_t settings_night_mode = 88;
|
||||
constexpr uint16_t settings_starts = 89;
|
||||
constexpr uint16_t settings_ends = 90;
|
||||
constexpr uint16_t settings_night_brightness = 91;
|
||||
constexpr uint16_t settings_back_to_page_1 = 92;
|
||||
constexpr uint16_t settings_after = 93;
|
||||
constexpr uint16_t settings_also_on_standby = 94;
|
||||
constexpr uint16_t settings_swipe_between_pages = 95;
|
||||
constexpr uint16_t settings_page_buttons = 96;
|
||||
constexpr uint16_t settings_rotation = 97;
|
||||
constexpr uint16_t settings_address = 98;
|
||||
constexpr uint16_t settings_firmware = 99;
|
||||
constexpr uint16_t settings_home_assistant = 100;
|
||||
constexpr uint16_t settings_restart = 101;
|
||||
constexpr uint16_t settings_tap_again_to_restart = 102;
|
||||
constexpr uint16_t settings_seconds = 103;
|
||||
constexpr uint16_t settings_minutes = 104;
|
||||
constexpr uint16_t settings_hours = 105;
|
||||
constexpr uint16_t settings_hours_minutes = 106;
|
||||
constexpr uint16_t tile_tap_to_open = 107;
|
||||
constexpr uint16_t tile_page = 108;
|
||||
constexpr uint16_t tile_unavailable = 109;
|
||||
constexpr uint16_t tile_refused = 110;
|
||||
constexpr uint16_t tile_command_sent = 111;
|
||||
constexpr uint16_t tile_confirmed = 112;
|
||||
constexpr uint16_t alert_notification = 113;
|
||||
constexpr uint16_t alert_ok = 114;
|
||||
constexpr uint16_t light_color = 115;
|
||||
constexpr uint16_t light_color_temperature = 116;
|
||||
constexpr uint16_t light_brightness = 117;
|
||||
constexpr uint16_t light_waiting = 118;
|
||||
constexpr uint16_t effects_effect = 119;
|
||||
constexpr uint16_t effects_nothing = 120;
|
||||
constexpr uint16_t effects_loading = 121;
|
||||
constexpr uint16_t effects_not_connected = 122;
|
||||
constexpr uint16_t climate_target = 123;
|
||||
constexpr uint16_t climate_mode = 124;
|
||||
constexpr uint16_t climate_fan = 125;
|
||||
constexpr uint16_t climate_swing = 126;
|
||||
constexpr uint16_t climate_now = 127;
|
||||
constexpr uint16_t vacuum_check_robot = 128;
|
||||
constexpr uint16_t vacuum_auto_suction = 129;
|
||||
constexpr uint16_t vacuum_robot_chooses = 130;
|
||||
constexpr uint16_t vacuum_per_room = 131;
|
||||
constexpr uint16_t vacuum_speed_quiet = 132;
|
||||
constexpr uint16_t vacuum_speed_balanced = 133;
|
||||
constexpr uint16_t vacuum_speed_turbo = 134;
|
||||
constexpr uint16_t vacuum_speed_max = 135;
|
||||
constexpr uint16_t vacuum_pause_cleaning = 136;
|
||||
constexpr uint16_t vacuum_pause = 137;
|
||||
constexpr uint16_t vacuum_resume = 138;
|
||||
constexpr uint16_t vacuum_start_cleaning = 139;
|
||||
constexpr uint16_t vacuum_clean = 140;
|
||||
constexpr uint16_t vacuum_dock = 141;
|
||||
constexpr uint16_t cover_position = 142;
|
||||
constexpr uint16_t cover_tilt = 143;
|
||||
constexpr uint16_t cover_tilt_value = 144;
|
||||
constexpr uint16_t weather_feels_like = 145;
|
||||
constexpr uint16_t weather_no_forecast = 146;
|
||||
constexpr uint16_t weather_coming_days = 147;
|
||||
constexpr uint16_t history_no_data = 148;
|
||||
constexpr uint16_t history_average = 149;
|
||||
constexpr uint16_t history_average_of = 150;
|
||||
constexpr uint16_t history_now = 151;
|
||||
constexpr uint16_t history_range_hour = 152;
|
||||
constexpr uint16_t history_range_day = 153;
|
||||
constexpr uint16_t history_range_week = 154;
|
||||
constexpr uint16_t history_high = 155;
|
||||
constexpr uint16_t history_low = 156;
|
||||
constexpr uint16_t history_times = 157;
|
||||
constexpr uint16_t history_loading = 158;
|
||||
constexpr uint16_t history_empty = 159;
|
||||
constexpr uint16_t history_unavailable = 160;
|
||||
constexpr uint16_t media_muted = 161;
|
||||
constexpr uint16_t media_not_playing = 162;
|
||||
constexpr uint16_t media_loading = 163;
|
||||
constexpr uint16_t timer_running = 164;
|
||||
constexpr uint16_t timer_paused = 165;
|
||||
constexpr uint16_t timer_stopped = 166;
|
||||
constexpr uint16_t timer_pause = 167;
|
||||
constexpr uint16_t timer_start = 168;
|
||||
constexpr uint16_t timer_cancel = 169;
|
||||
constexpr uint16_t timer_paused_left = 170;
|
||||
constexpr uint16_t sun_name = 171;
|
||||
constexpr uint16_t sun_sunrise = 172;
|
||||
constexpr uint16_t sun_sunset = 173;
|
||||
constexpr uint16_t sun_rise = 174;
|
||||
constexpr uint16_t sun_set = 175;
|
||||
constexpr uint16_t script_running = 176;
|
||||
constexpr uint16_t script_never_run = 177;
|
||||
constexpr uint16_t script_last_time = 178;
|
||||
constexpr uint16_t script_yesterday_time = 179;
|
||||
constexpr uint16_t script_last_date = 180;
|
||||
constexpr uint16_t camera_no_image = 181;
|
||||
constexpr uint16_t camera_loading = 182;
|
||||
constexpr uint16_t camera_live = 183;
|
||||
constexpr uint16_t camera_recording = 184;
|
||||
constexpr uint16_t camera_tap_to_view = 185;
|
||||
constexpr uint16_t camera_no_image_yet = 186;
|
||||
constexpr uint16_t button_activate = 187;
|
||||
constexpr uint16_t button_run = 188;
|
||||
constexpr uint16_t button_press = 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_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 date_weekdays = 16;
|
||||
constexpr uint16_t date_weekdays_count = 7;
|
||||
constexpr uint16_t date_weekdays_short = 23;
|
||||
constexpr uint16_t date_weekdays_short_count = 7;
|
||||
constexpr uint16_t date_weekdays_min = 30;
|
||||
constexpr uint16_t date_weekdays_min_count = 7;
|
||||
constexpr uint16_t date_months = 37;
|
||||
constexpr uint16_t date_months_count = 12;
|
||||
constexpr uint16_t date_months_short = 49;
|
||||
constexpr uint16_t date_months_short_count = 12;
|
||||
} // namespace txt
|
||||
constexpr uint16_t KEY_COUNT = 302;
|
||||
constexpr uint32_t KEYS_HASH = 0x725A92EEu;
|
||||
} // namespace screen_text
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include "screen_settings.h"
|
||||
#include "screen_text.h"
|
||||
|
||||
// The settings page on the screen itself (firmware 0.2.44+). It changes the same values ESP Screen
|
||||
// Manager writes, for the moments you stand in front of the panel instead of behind a browser.
|
||||
@@ -34,15 +35,19 @@ using Text = std::string (*)();
|
||||
using Shown = bool (*)();
|
||||
using Run = void (*)();
|
||||
|
||||
// The page's words are keys into the screen's language (screen.settings in screen_manager/translations, app 0.2.90):
|
||||
// the table stays constexpr, the text is looked up when a row is drawn.
|
||||
constexpr uint16_t NO_TEXT = 0xFFFF;
|
||||
struct Row {
|
||||
Kind kind = Kind::info;
|
||||
const char *label = "";
|
||||
uint16_t label = NO_TEXT;
|
||||
const char *icon = ""; // page rows and actions; a Material Design Icons glyph
|
||||
Read read = nullptr; // toggle, number, duration, moment, choice
|
||||
Write write = nullptr;
|
||||
int32_t low = 0, high = 0, step = 0; // number: fixed step; duration: seconds, step from the ladder
|
||||
const char *unit = ""; // straight after a number ("%")
|
||||
const char *const *options = nullptr; // choice
|
||||
const char *const *options = nullptr; // choice with the same words in every language ("90°")
|
||||
uint16_t option_keys = NO_TEXT; // choice in words: the key of its first option, the others follow it
|
||||
uint8_t option_count = 0;
|
||||
Text text = nullptr; // info
|
||||
Run run = nullptr; // action
|
||||
@@ -51,35 +56,40 @@ struct Row {
|
||||
uint8_t opens = 0; // page rows: the page they open
|
||||
};
|
||||
|
||||
constexpr Row page_row(const char *label, const char *icon, uint8_t opens) {
|
||||
constexpr Row page_row(uint16_t label, const char *icon, uint8_t opens) {
|
||||
Row r{}; r.kind = Kind::page; r.label = label; r.icon = icon; r.opens = opens; return r;
|
||||
}
|
||||
constexpr Row toggle(const char *label, Read read, Write write, Shown shown = nullptr) {
|
||||
constexpr Row toggle(uint16_t label, Read read, Write write, Shown shown = nullptr) {
|
||||
Row r{}; r.kind = Kind::toggle; r.label = label; r.read = read; r.write = write; r.shown = shown; return r;
|
||||
}
|
||||
constexpr Row number(const char *label, Read read, Write write, int32_t low, int32_t high, int32_t step,
|
||||
constexpr Row number(uint16_t label, Read read, Write write, int32_t low, int32_t high, int32_t step,
|
||||
const char *unit = "", Shown enabled = nullptr) {
|
||||
Row r{}; r.kind = Kind::number; r.label = label; r.read = read; r.write = write;
|
||||
r.low = low; r.high = high; r.step = step; r.unit = unit; r.enabled = enabled; return r;
|
||||
}
|
||||
constexpr Row duration(const char *label, Read read, Write write, int32_t low, int32_t high,
|
||||
constexpr Row duration(uint16_t label, Read read, Write write, int32_t low, int32_t high,
|
||||
Shown enabled = nullptr) {
|
||||
Row r{}; r.kind = Kind::duration; r.label = label; r.read = read; r.write = write;
|
||||
r.low = low; r.high = high; r.enabled = enabled; return r;
|
||||
}
|
||||
constexpr Row moment(const char *label, Read read, Write write, Shown enabled = nullptr) {
|
||||
constexpr Row moment(uint16_t label, Read read, Write write, Shown enabled = nullptr) {
|
||||
Row r{}; r.kind = Kind::moment; r.label = label; r.read = read; r.write = write;
|
||||
r.low = 0; r.high = 1439; r.step = 15; r.enabled = enabled; return r;
|
||||
}
|
||||
constexpr Row choice(const char *label, Read read, Write write, const char *const *options, uint8_t count,
|
||||
constexpr Row choice(uint16_t label, Read read, Write write, const char *const *options, uint8_t count,
|
||||
Shown shown = nullptr) {
|
||||
Row r{}; r.kind = Kind::choice; r.label = label; r.read = read; r.write = write;
|
||||
r.options = options; r.option_count = count; r.shown = shown; return r;
|
||||
}
|
||||
constexpr Row info(const char *label, Text text) {
|
||||
constexpr Row choice(uint16_t label, Read read, Write write, uint16_t option_keys, uint8_t count, Shown shown = nullptr) {
|
||||
Row r{}; r.kind = Kind::choice; r.label = label; r.read = read; r.write = write;
|
||||
r.option_keys = option_keys; r.option_count = count; r.shown = shown; return r;
|
||||
}
|
||||
inline const char *label_text(const Row &row) { return screen_text::tr(row.label); }
|
||||
constexpr Row info(uint16_t label, Text text) {
|
||||
Row r{}; r.kind = Kind::info; r.label = label; r.text = text; return r;
|
||||
}
|
||||
constexpr Row action(const char *label, const char *icon, Run run) {
|
||||
constexpr Row action(uint16_t label, const char *icon, Run run) {
|
||||
Row r{}; r.kind = Kind::action; r.label = label; r.icon = icon; r.run = run; return r;
|
||||
}
|
||||
|
||||
@@ -169,26 +179,28 @@ inline int knob_x(int w, int h, bool on) {
|
||||
}
|
||||
|
||||
inline std::string duration_text(int32_t seconds) {
|
||||
char buffer[24];
|
||||
if (seconds < 60) { snprintf(buffer, sizeof(buffer), "%d sec", (int) seconds); return buffer; }
|
||||
if (seconds < 3600) { snprintf(buffer, sizeof(buffer), "%d min", (int) (seconds / 60)); return buffer; }
|
||||
using namespace screen_text;
|
||||
if (seconds < 60) return fill(txt::settings_seconds, "n", (int) seconds);
|
||||
if (seconds < 3600) return fill(txt::settings_minutes, "n", (int) (seconds / 60));
|
||||
int hours = seconds / 3600, minutes = (seconds % 3600) / 60;
|
||||
if (minutes) snprintf(buffer, sizeof(buffer), "%d h %02d", hours, minutes);
|
||||
else snprintf(buffer, sizeof(buffer), "%d h", hours);
|
||||
return buffer;
|
||||
if (!minutes) return fill(txt::settings_hours, "n", hours);
|
||||
char two[4];
|
||||
snprintf(two, sizeof(two), "%02d", minutes);
|
||||
return fill(fill(txt::settings_hours_minutes, "h", hours), "m", two);
|
||||
}
|
||||
// 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, hour < 12 ? "AM" : "PM");
|
||||
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;
|
||||
}
|
||||
// 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 row.read && row.read() ? "On" : "Off";
|
||||
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);
|
||||
@@ -198,7 +210,8 @@ inline std::string value_text(const Row &row) {
|
||||
case Kind::moment: return moment_text(row.read ? row.read() : 0, screen_settings::current.clock_24h != 0);
|
||||
case Kind::choice: {
|
||||
int32_t index = row.read ? row.read() : 0;
|
||||
return index >= 0 && index < row.option_count ? row.options[index] : "";
|
||||
if (index < 0 || index >= row.option_count) return "";
|
||||
return row.option_keys != NO_TEXT ? screen_text::tr(row.option_keys + index) : row.options[index];
|
||||
}
|
||||
case Kind::info: return row.text ? row.text() : "";
|
||||
default: return "";
|
||||
@@ -208,7 +221,7 @@ inline bool visible_row(const Row &row) { return !row.shown || row.shown(); }
|
||||
inline bool live_row(const Row &row) { return !row.enabled || row.enabled(); }
|
||||
|
||||
struct Page {
|
||||
const char *title;
|
||||
uint16_t title;
|
||||
const Row *rows;
|
||||
uint8_t count;
|
||||
};
|
||||
@@ -223,7 +236,6 @@ inline uint8_t fitting_rows(int span, int row_height, int gap, int pager, uint8_
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ the pages
|
||||
inline constexpr const char *clock_options[] = {"12 hour", "24 hour"};
|
||||
inline constexpr const char *rotation_options[] = {"0°", "90°", "180°", "270°"};
|
||||
|
||||
// Brightness, the dark look for a screen beside a bed, and when the screen dims by itself.
|
||||
@@ -231,52 +243,51 @@ inline constexpr const char *rotation_options[] = {"0°", "90°", "180°", "270
|
||||
// plain `int` (a ternary, say) then does not convert to Read at all. The Mac's host build accepts it,
|
||||
// the board does not.
|
||||
inline constexpr Row light_rows[] = {
|
||||
number("Brightness", []() -> int32_t { return screen_settings::current.brightness; },
|
||||
number(screen_text::txt::settings_brightness, []() -> int32_t { return screen_settings::current.brightness; },
|
||||
[](int32_t value) { set("brightness", value); }, 5, 100, 5, "%"),
|
||||
toggle("Dark mode", []() -> int32_t { return dark_mode; },
|
||||
toggle(screen_text::txt::settings_dark_mode, []() -> int32_t { return dark_mode; },
|
||||
[](int32_t value) { set("dark_mode", value); }),
|
||||
toggle("Auto standby", []() -> int32_t { return screen_settings::current.standby_enabled; },
|
||||
toggle(screen_text::txt::settings_auto_standby, []() -> int32_t { return screen_settings::current.standby_enabled; },
|
||||
[](int32_t value) { set("standby_enabled", value); }),
|
||||
duration("Standby after", []() -> int32_t { return screen_settings::current.standby_seconds; },
|
||||
duration(screen_text::txt::settings_standby_after, []() -> int32_t { return screen_settings::current.standby_seconds; },
|
||||
[](int32_t value) { set("standby_seconds", value); }, 60, 86400,
|
||||
[] { return screen_settings::current.standby_enabled != 0; }),
|
||||
number("Standby brightness", []() -> int32_t { return screen_settings::current.standby_brightness; },
|
||||
number(screen_text::txt::settings_standby_brightness, []() -> int32_t { return screen_settings::current.standby_brightness; },
|
||||
[](int32_t value) { set("standby_brightness", value); }, 0, 100, 5, "%",
|
||||
[] { return screen_settings::current.standby_enabled != 0; }),
|
||||
};
|
||||
|
||||
// Darker between two times, so a panel in a hallway does not light up the bedroom.
|
||||
inline constexpr Row night_rows[] = {
|
||||
toggle("Night mode", []() -> int32_t { return screen_settings::current.night_enabled; },
|
||||
toggle(screen_text::txt::settings_night_mode, []() -> int32_t { return screen_settings::current.night_enabled; },
|
||||
[](int32_t value) { set("night_enabled", value); }),
|
||||
moment("Starts", []() -> int32_t { return screen_settings::current.night_start; },
|
||||
moment(screen_text::txt::settings_starts, []() -> int32_t { return screen_settings::current.night_start; },
|
||||
[](int32_t value) { set("night_start", value); },
|
||||
[] { return screen_settings::current.night_enabled != 0; }),
|
||||
moment("Ends", []() -> int32_t { return screen_settings::current.night_end; },
|
||||
moment(screen_text::txt::settings_ends, []() -> int32_t { return screen_settings::current.night_end; },
|
||||
[](int32_t value) { set("night_end", value); },
|
||||
[] { return screen_settings::current.night_enabled != 0; }),
|
||||
number("Night brightness", []() -> int32_t { return screen_settings::current.night_brightness; },
|
||||
number(screen_text::txt::settings_night_brightness, []() -> int32_t { return screen_settings::current.night_brightness; },
|
||||
[](int32_t value) { set("night_brightness", value); }, 0, 100, 5, "%",
|
||||
[] { return screen_settings::current.night_enabled != 0; }),
|
||||
};
|
||||
|
||||
// How the screen behaves under your finger: the clock, going back to the first page, swiping, the page buttons,
|
||||
// turning.
|
||||
// How the screen behaves under your finger: going back to the first page, swiping, the page buttons, turning. The
|
||||
// clock's 12 or 24 hours is no longer a setting of each screen (app 0.2.90): ESP Screens sends it with the number
|
||||
// format, from Settings -> Language & region, the one place for every screen.
|
||||
inline constexpr Row screen_rows[] = {
|
||||
choice("Clock", []() -> int32_t { return screen_settings::current.clock_24h ? 1 : 0; },
|
||||
[](int32_t value) { set("clock_24h", value); }, clock_options, 2),
|
||||
toggle("Back to page 1", []() -> int32_t { return auto_home; },
|
||||
toggle(screen_text::txt::settings_back_to_page_1, []() -> int32_t { return auto_home; },
|
||||
[](int32_t value) { set("auto_home", value); }),
|
||||
duration("After", []() -> int32_t { return auto_home_seconds; },
|
||||
duration(screen_text::txt::settings_after, []() -> int32_t { return auto_home_seconds; },
|
||||
[](int32_t value) { set("auto_home_seconds", value); }, 30, 3600,
|
||||
[] { return auto_home != 0; }),
|
||||
toggle("Also on standby", []() -> int32_t { return screen_settings::current.home_on_standby; },
|
||||
toggle(screen_text::txt::settings_also_on_standby, []() -> int32_t { return screen_settings::current.home_on_standby; },
|
||||
[](int32_t value) { set("home_on_standby", value); }),
|
||||
toggle("Swipe between pages", []() -> int32_t { return swipe_pages; },
|
||||
toggle(screen_text::txt::settings_swipe_between_pages, []() -> int32_t { return swipe_pages; },
|
||||
[](int32_t value) { set("swipe_pages", value); }),
|
||||
toggle("Page buttons", []() -> int32_t { return page_buttons; },
|
||||
toggle(screen_text::txt::settings_page_buttons, []() -> int32_t { return page_buttons; },
|
||||
[](int32_t value) { set("page_buttons", value); }),
|
||||
choice("Rotation", []() -> int32_t { return rotation / 90; },
|
||||
choice(screen_text::txt::settings_rotation, []() -> int32_t { return rotation / 90; },
|
||||
[](int32_t value) { set("rotation", std::clamp<int32_t>(value, 0, 3) * 90); },
|
||||
rotation_options, 4, [] { return rotation_supported; }),
|
||||
};
|
||||
@@ -288,26 +299,26 @@ inline std::string (*firmware_text)() = nullptr;
|
||||
inline std::string (*link_text)() = nullptr;
|
||||
inline void (*restart_device)() = nullptr;
|
||||
inline constexpr Row about_rows[] = {
|
||||
info("Screen", [] { return name_text ? name_text() : std::string(); }),
|
||||
info("Address", [] { return address_text ? address_text() : std::string(); }),
|
||||
info("Firmware", [] { return firmware_text ? firmware_text() : std::string(); }),
|
||||
info("Home Assistant", [] { return link_text ? link_text() : std::string(); }),
|
||||
action("Restart", "\U000F0709", [] { if (restart_device) restart_device(); }),
|
||||
info(screen_text::txt::settings_screen, [] { return name_text ? name_text() : std::string(); }),
|
||||
info(screen_text::txt::settings_address, [] { return address_text ? address_text() : std::string(); }),
|
||||
info(screen_text::txt::settings_firmware, [] { return firmware_text ? firmware_text() : std::string(); }),
|
||||
info(screen_text::txt::settings_home_assistant, [] { return link_text ? link_text() : std::string(); }),
|
||||
action(screen_text::txt::settings_restart, "\U000F0709", [] { if (restart_device) restart_device(); }),
|
||||
};
|
||||
|
||||
inline constexpr Row menu_rows[] = {
|
||||
page_row("Brightness", "\U000F0599", 1),
|
||||
page_row("Night", "\U000F0594", 2),
|
||||
page_row("Screen", "\U000F0379", 3),
|
||||
page_row("This screen", "\U000F02FD", 4),
|
||||
page_row(screen_text::txt::settings_brightness, "\U000F0599", 1),
|
||||
page_row(screen_text::txt::settings_night, "\U000F0594", 2),
|
||||
page_row(screen_text::txt::settings_screen, "\U000F0379", 3),
|
||||
page_row(screen_text::txt::settings_this_screen, "\U000F02FD", 4),
|
||||
};
|
||||
|
||||
inline constexpr Page pages[] = {
|
||||
{"Settings", menu_rows, (uint8_t) std::size(menu_rows)},
|
||||
{"Brightness", light_rows, (uint8_t) std::size(light_rows)},
|
||||
{"Night", night_rows, (uint8_t) std::size(night_rows)},
|
||||
{"Screen", screen_rows, (uint8_t) std::size(screen_rows)},
|
||||
{"This screen", about_rows, (uint8_t) std::size(about_rows)},
|
||||
{screen_text::txt::settings_title, menu_rows, (uint8_t) std::size(menu_rows)},
|
||||
{screen_text::txt::settings_brightness, light_rows, (uint8_t) std::size(light_rows)},
|
||||
{screen_text::txt::settings_night, night_rows, (uint8_t) std::size(night_rows)},
|
||||
{screen_text::txt::settings_screen, screen_rows, (uint8_t) std::size(screen_rows)},
|
||||
{screen_text::txt::settings_this_screen, about_rows, (uint8_t) std::size(about_rows)},
|
||||
};
|
||||
constexpr uint8_t PAGE_COUNT = (uint8_t) std::size(pages);
|
||||
|
||||
@@ -587,7 +598,7 @@ inline void draw() {
|
||||
lv_obj_center(arrow);
|
||||
lv_obj_add_event_cb(back, back_event, LV_EVENT_SHORT_CLICKED, nullptr);
|
||||
const lv_font_t *heading = title_font ? title_font : row_font;
|
||||
auto *title = text(root, page.title, heading, theme::INK, LV_TEXT_ALIGN_CENTER);
|
||||
auto *title = text(root, screen_text::tr(page.title), heading, theme::INK, LV_TEXT_ALIGN_CENTER);
|
||||
lv_obj_set_width(title, m.width - 2 * (m.pad + m.bar + 8));
|
||||
lv_obj_set_pos(title, m.pad + m.bar + 8, m.bar_y + (m.bar - lv_font_get_line_height(heading)) / 2);
|
||||
|
||||
@@ -628,7 +639,8 @@ inline void draw() {
|
||||
lv_obj_set_pos(glyph, left, (m.row_h - icon_h) / 2);
|
||||
left += icon_h + (m.large ? 12 : 8);
|
||||
}
|
||||
d.label = text(d.card, asking ? "Tap again to restart" : row.label, row_font, asking ? theme::ON_ACCENT : theme::INK);
|
||||
d.label = text(d.card, screen_text::tr(asking ? screen_text::txt::settings_tap_again_to_restart : row.label), row_font,
|
||||
asking ? theme::ON_ACCENT : theme::INK);
|
||||
lv_obj_set_pos(d.label, left, (m.row_h - label_h) / 2);
|
||||
|
||||
int right = m.width - 2 * m.pad - m.inset; // free space from the right edge of the card
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// a -/+ step lands on the entity's grid, and the status line beside them. The
|
||||
// LVGL drawing lives in runtime_tiles.h; tests/test_tile_controls.cpp covers this.
|
||||
#include "runtime_model.h"
|
||||
#include "screen_text.h"
|
||||
#include "theme.h"
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
@@ -157,69 +158,88 @@ inline float step_value(float current, float step, float minimum, float maximum,
|
||||
if (has_max) next = std::fmin(next, maximum);
|
||||
return next;
|
||||
}
|
||||
// Whole degrees when the entity steps by whole units, one decimal otherwise.
|
||||
// Whole degrees when the entity steps by whole units, one decimal otherwise, with the language's decimal mark.
|
||||
inline std::string format_value(float value, float step, const char *suffix) {
|
||||
if (!std::isfinite(value)) return "--";
|
||||
char b[24]; snprintf(b, sizeof(b), step >= 1 ? "%.0f%s" : "%.1f%s", value, suffix); return b;
|
||||
return screen_text::decimal(value, step >= 1 ? 0 : 1) + suffix;
|
||||
}
|
||||
inline const char *climate_mode_text(const std::string &mode) {
|
||||
if (mode == "off") return "Off";
|
||||
if (mode == "heat") return "Heat";
|
||||
if (mode == "cool") return "Cool";
|
||||
if (mode == "heat_cool") return "Heat/Cool";
|
||||
if (mode == "auto") return "Auto";
|
||||
if (mode == "dry") return "Dry";
|
||||
if (mode == "fan_only") return "Fan only";
|
||||
if (mode == "off") return screen_text::tr(screen_text::txt::ha_climate_off);
|
||||
if (mode == "heat") return screen_text::tr(screen_text::txt::ha_climate_heat);
|
||||
if (mode == "cool") return screen_text::tr(screen_text::txt::ha_climate_cool);
|
||||
if (mode == "heat_cool") return screen_text::tr(screen_text::txt::ha_climate_heat_cool);
|
||||
if (mode == "auto") return screen_text::tr(screen_text::txt::ha_climate_auto);
|
||||
if (mode == "dry") return screen_text::tr(screen_text::txt::ha_climate_dry);
|
||||
if (mode == "fan_only") return screen_text::tr(screen_text::txt::ha_climate_fan_only);
|
||||
return mode.c_str();
|
||||
}
|
||||
inline const char *climate_action_text(const std::string &action) {
|
||||
if (action == "heating") return "Heating";
|
||||
if (action == "cooling") return "Cooling";
|
||||
if (action == "idle") return "Idle";
|
||||
if (action == "off") return "Off";
|
||||
if (action == "drying") return "Drying";
|
||||
if (action == "fan") return "Fan";
|
||||
if (action == "preheating") return "Preheating";
|
||||
if (action == "defrosting") return "Defrosting";
|
||||
if (action == "heating") return screen_text::tr(screen_text::txt::ha_hvac_action_heating);
|
||||
if (action == "cooling") return screen_text::tr(screen_text::txt::ha_hvac_action_cooling);
|
||||
if (action == "idle") return screen_text::tr(screen_text::txt::ha_hvac_action_idle);
|
||||
if (action == "off") return screen_text::tr(screen_text::txt::ha_hvac_action_off);
|
||||
if (action == "drying") return screen_text::tr(screen_text::txt::ha_hvac_action_drying);
|
||||
if (action == "fan") return screen_text::tr(screen_text::txt::ha_hvac_action_fan);
|
||||
if (action == "preheating") return screen_text::tr(screen_text::txt::ha_hvac_action_preheating);
|
||||
if (action == "defrosting") return screen_text::tr(screen_text::txt::ha_hvac_action_defrosting);
|
||||
return "";
|
||||
}
|
||||
inline const char *cover_state_text(const std::string &state) {
|
||||
if (state == "open") return "Open";
|
||||
if (state == "closed") return "Closed";
|
||||
if (state == "opening") return "Opening";
|
||||
if (state == "closing") return "Closing";
|
||||
if (state == "open") return screen_text::tr(screen_text::txt::ha_cover_open);
|
||||
if (state == "closed") return screen_text::tr(screen_text::txt::ha_cover_closed);
|
||||
if (state == "opening") return screen_text::tr(screen_text::txt::ha_cover_opening);
|
||||
if (state == "closing") return screen_text::tr(screen_text::txt::ha_cover_closing);
|
||||
return state.c_str();
|
||||
}
|
||||
inline const char *media_state_text(const std::string &state) {
|
||||
if (state == "playing") return "Playing";
|
||||
if (state == "paused") return "Paused";
|
||||
if (state == "idle") return "Idle";
|
||||
if (state == "standby") return "Standby";
|
||||
if (state == "buffering") return "Loading";
|
||||
if (state == "on") return "On";
|
||||
if (state == "off") return "Off";
|
||||
if (state == "playing") return screen_text::tr(screen_text::txt::ha_media_playing);
|
||||
if (state == "paused") return screen_text::tr(screen_text::txt::ha_media_paused);
|
||||
if (state == "idle") return screen_text::tr(screen_text::txt::ha_media_idle);
|
||||
if (state == "standby") return screen_text::tr(screen_text::txt::ha_media_standby);
|
||||
if (state == "buffering") return screen_text::tr(screen_text::txt::media_loading);
|
||||
if (state == "on") return screen_text::tr(screen_text::txt::ha_on);
|
||||
if (state == "off") return screen_text::tr(screen_text::txt::ha_off);
|
||||
return state.c_str();
|
||||
}
|
||||
// A binary sensor's state in Home Assistant's words for its device class: a door is Open or Closed, a leak sensor
|
||||
// Wet or Dry. The same words as the add-on's header_bar.BINARY_STATES, which the top bar and the history card show;
|
||||
// tests/test_binary_words.py keeps the two tables equal. Without a class, or with one this table lacks: On and Off.
|
||||
struct BinaryWords { const char *device_class, *on, *off; };
|
||||
// Wet or Dry, in the screen's language (screen.ha.binary, taken from Home Assistant's own translations). The add-on's
|
||||
// header_bar.BINARY_STATES reads the same words from the same file for the top bar and the history card. Without a
|
||||
// class, or with one this table lacks: On and Off.
|
||||
struct BinaryWords { const char *device_class; uint16_t on, off; };
|
||||
constexpr BinaryWords BINARY_WORDS[] = {
|
||||
{"battery", "Low", "Normal"}, {"battery_charging", "Charging", "Not charging"},
|
||||
{"carbon_monoxide", "Danger", "Safe"}, {"cold", "Cold", "Normal"}, {"connectivity", "Connected", "Disconnected"},
|
||||
{"door", "Open", "Closed"}, {"garage_door", "Open", "Closed"}, {"gas", "Danger", "Safe"},
|
||||
{"heat", "Hot", "Normal"}, {"light", "Light", "Dark"}, {"lock", "Open", "Locked"}, {"moisture", "Wet", "Dry"},
|
||||
{"motion", "Motion", "No motion"}, {"moving", "Moving", "Still"}, {"occupancy", "Occupied", "Clear"},
|
||||
{"opening", "Open", "Closed"}, {"plug", "Plugged in", "Unplugged"}, {"power", "On", "Off"},
|
||||
{"presence", "Home", "Away"}, {"problem", "Problem", "OK"}, {"running", "Active", "Inactive"},
|
||||
{"safety", "Unsafe", "Safe"}, {"smoke", "Smoke", "No smoke"}, {"sound", "Sound", "Silent"},
|
||||
{"tamper", "Tampering", "OK"}, {"update", "Update", "Up to date"}, {"vibration", "Vibration", "Still"},
|
||||
{"window", "Open", "Closed"},
|
||||
{"battery", screen_text::txt::ha_binary_battery_on, screen_text::txt::ha_binary_battery_off},
|
||||
{"battery_charging", screen_text::txt::ha_binary_battery_charging_on, screen_text::txt::ha_binary_battery_charging_off},
|
||||
{"carbon_monoxide", screen_text::txt::ha_binary_carbon_monoxide_on, screen_text::txt::ha_binary_carbon_monoxide_off},
|
||||
{"cold", screen_text::txt::ha_binary_cold_on, screen_text::txt::ha_binary_cold_off},
|
||||
{"connectivity", screen_text::txt::ha_binary_connectivity_on, screen_text::txt::ha_binary_connectivity_off},
|
||||
{"door", screen_text::txt::ha_binary_door_on, screen_text::txt::ha_binary_door_off},
|
||||
{"garage_door", screen_text::txt::ha_binary_garage_door_on, screen_text::txt::ha_binary_garage_door_off},
|
||||
{"gas", screen_text::txt::ha_binary_gas_on, screen_text::txt::ha_binary_gas_off},
|
||||
{"heat", screen_text::txt::ha_binary_heat_on, screen_text::txt::ha_binary_heat_off},
|
||||
{"light", screen_text::txt::ha_binary_light_on, screen_text::txt::ha_binary_light_off},
|
||||
{"lock", screen_text::txt::ha_binary_lock_on, screen_text::txt::ha_binary_lock_off},
|
||||
{"moisture", screen_text::txt::ha_binary_moisture_on, screen_text::txt::ha_binary_moisture_off},
|
||||
{"motion", screen_text::txt::ha_binary_motion_on, screen_text::txt::ha_binary_motion_off},
|
||||
{"moving", screen_text::txt::ha_binary_moving_on, screen_text::txt::ha_binary_moving_off},
|
||||
{"occupancy", screen_text::txt::ha_binary_occupancy_on, screen_text::txt::ha_binary_occupancy_off},
|
||||
{"opening", screen_text::txt::ha_binary_opening_on, screen_text::txt::ha_binary_opening_off},
|
||||
{"plug", screen_text::txt::ha_binary_plug_on, screen_text::txt::ha_binary_plug_off},
|
||||
{"power", screen_text::txt::ha_binary_power_on, screen_text::txt::ha_binary_power_off},
|
||||
{"presence", screen_text::txt::ha_binary_presence_on, screen_text::txt::ha_binary_presence_off},
|
||||
{"problem", screen_text::txt::ha_binary_problem_on, screen_text::txt::ha_binary_problem_off},
|
||||
{"running", screen_text::txt::ha_binary_running_on, screen_text::txt::ha_binary_running_off},
|
||||
{"safety", screen_text::txt::ha_binary_safety_on, screen_text::txt::ha_binary_safety_off},
|
||||
{"smoke", screen_text::txt::ha_binary_smoke_on, screen_text::txt::ha_binary_smoke_off},
|
||||
{"sound", screen_text::txt::ha_binary_sound_on, screen_text::txt::ha_binary_sound_off},
|
||||
{"tamper", screen_text::txt::ha_binary_tamper_on, screen_text::txt::ha_binary_tamper_off},
|
||||
{"update", screen_text::txt::ha_binary_update_on, screen_text::txt::ha_binary_update_off},
|
||||
{"vibration", screen_text::txt::ha_binary_vibration_on, screen_text::txt::ha_binary_vibration_off},
|
||||
{"window", screen_text::txt::ha_binary_window_on, screen_text::txt::ha_binary_window_off},
|
||||
};
|
||||
inline const char *binary_state_text(const std::string &device_class, bool on) {
|
||||
for (const auto &words : BINARY_WORDS)
|
||||
if (device_class == words.device_class) return on ? words.on : words.off;
|
||||
return on ? "On" : "Off";
|
||||
if (device_class == words.device_class) return screen_text::tr(on ? words.on : words.off);
|
||||
return screen_text::tr(on ? screen_text::txt::ha_on : screen_text::txt::ha_off);
|
||||
}
|
||||
// Status line beside a control panel: what Home Assistant shows under the name.
|
||||
inline std::string status_text(const Tile &t) {
|
||||
@@ -227,7 +247,7 @@ inline std::string status_text(const Tile &t) {
|
||||
if (d == "climate") {
|
||||
std::string text = climate_action_text(t.extra().hvac_action);
|
||||
if (text.empty()) text = climate_mode_text(t.state);
|
||||
if (std::isfinite(t.current)) { snprintf(b, sizeof(b), " · %.1f°", t.current); text += b; }
|
||||
if (std::isfinite(t.current)) text += " · " + screen_text::decimal(t.current, 1) + "°";
|
||||
return text;
|
||||
}
|
||||
if (d == "cover") {
|
||||
@@ -261,7 +281,7 @@ inline CoverCard cover_card(const Tile &t) {
|
||||
inline std::string cover_card_status(const Tile &t) {
|
||||
std::string text = status_text(t);
|
||||
if ((t.supported & feature::COVER_TILT_POSITION) && std::isfinite(t.extra().tilt)) {
|
||||
char b[24]; snprintf(b, sizeof(b), " · Tilt %d%%", (int) std::lround(t.extra().tilt)); text += b;
|
||||
text += " · " + screen_text::fill(screen_text::txt::cover_tilt_value, "n", (int) std::lround(t.extra().tilt));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -328,9 +348,9 @@ inline std::string neighbour_option(const Tile &t, int direction) {
|
||||
return options[(current + direction + count) % count];
|
||||
}
|
||||
inline const char *run_label(const std::string &domain) {
|
||||
if (domain == "scene") return "Activate";
|
||||
if (domain == "script") return "Run";
|
||||
return "Press";
|
||||
if (domain == "scene") return screen_text::tr(screen_text::txt::button_activate);
|
||||
if (domain == "script") return screen_text::tr(screen_text::txt::button_run);
|
||||
return screen_text::tr(screen_text::txt::button_press);
|
||||
}
|
||||
// The Home Assistant action behind a key. STEP_* are handled locally (debounced) and return nothing.
|
||||
inline Action key_action(const Tile &t, int command, const std::string &arg = "") {
|
||||
@@ -403,10 +423,10 @@ inline VacuumRows vacuum_rows(const Tile &t, uint32_t now) {
|
||||
}
|
||||
// Labels for a manager that sends no suction row (app before 0.2.46): the vacuum's own speed names.
|
||||
inline std::string speed_label(const std::string &speed) {
|
||||
if (speed == "quiet") return "Quiet";
|
||||
if (speed == "balanced") return "Normal";
|
||||
if (speed == "turbo") return "Turbo";
|
||||
if (speed == "max") return "Max";
|
||||
if (speed == "quiet") return screen_text::tr(screen_text::txt::vacuum_speed_quiet);
|
||||
if (speed == "balanced") return screen_text::tr(screen_text::txt::vacuum_speed_balanced);
|
||||
if (speed == "turbo") return screen_text::tr(screen_text::txt::vacuum_speed_turbo);
|
||||
if (speed == "max") return screen_text::tr(screen_text::txt::vacuum_speed_max);
|
||||
return speed;
|
||||
}
|
||||
// The suction row a vacuum card shows: the manager's (filtered, labelled) speeds, else the first four
|
||||
|
||||
Reference in New Issue
Block a user