Files
homeassistant_espscreen/components/smart_display/header_bar.h
T
Max Gramser 3e959b3df9 Put ESP Screens in English and keep layouts when entity IDs change (0.2.40)
Everything a user sees or reads is now English: both boards' labels,
status replies and logs, the ESP Screens editor and Alerts cheatsheet,
and the README and guides (docs/TILES.md, CALIBRATING.md, ACCEPTANCE.md
and TROUBLESHOOTING.md replace the Dutch file names). Labels follow
Home Assistant's own English names (Heat/Cool modes, Heating/Cooling
actions, Away, Returning to dock, weather conditions), dates use English
abbreviations and the top bar writes numbers the English way. Docs use
HA 2026.9's menu names (Settings -> Apps, App store, Settings -> Tools).
Unused legacy files (home-like.yaml, buttons.yaml, a.out) are removed.

Firmware 0.2.34 names its entities in English. Home Assistant registers
a renamed ESPHome entity under a new entity ID, so the inbox
text.<screen>_tegelinstellingen becomes text.<screen>_tile_settings and
the layout stored under the old ID would be lost. The manager now
recognises the screen by its device, during a run or after a restart,
and moves its layout and update history along; the Update button and
the nightly round follow the screen through the rename. Screens that
still run 0.2.33 or older keep working: both languages are matched.

Main is the only release branch from now on.

Tested on a Home Assistant Yellow (HA 2026.9.1) with both bench boards:
see docs/TEST_RESULTS_0240.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 02:08:43 +02:00

129 lines
5.6 KiB
C++

#pragma once
#include <algorithm>
#include <array>
#include <cstdint>
#include <string>
namespace header_bar {
// The top bar right of the screen name (firmware 0.2.32+). ESP Screen Manager decides what shows
// and writes an entity's text; the screen draws it, ticks its clocks and counts "5 min ago"
// itself. Everything here is free of LVGL, so tests/test_header_bar.cpp covers it on a PC.
constexpr size_t MAX_ITEMS = 6;
constexpr size_t TEXT_BYTES = 48;
enum class Kind : uint8_t { none, clock, analog, date, text, ago };
inline Kind kind(const std::string &name) {
if (name == "clock") return Kind::clock;
if (name == "analog") return Kind::analog;
if (name == "date") return Kind::date;
if (name == "text") return Kind::text;
if (name == "ago") return Kind::ago;
return Kind::none;
}
struct Item {
Kind kind = Kind::none;
uint32_t icon = 0; // Material Design Icons codepoint, 0 without an icon
std::string text; // Kind::text
int64_t epoch = 0; // Kind::ago: the moment, past or future
uint32_t color = 0; // accent of the icon while `has_color`
bool has_color = false;
bool operator==(const Item &o) const {
return kind == o.kind && icon == o.icon && text == o.text && epoch == o.epoch && color == o.color && has_color == o.has_color;
}
};
struct Bar {
std::array<Item, MAX_ITEMS> items{};
size_t count = 0;
bool received = false; // false until the manager sent one: the clock of show_clock then
};
// "RRGGBB" as 0xRRGGBB; false for anything else.
inline bool color(const std::string &hex, uint32_t &out) {
if (hex.size() != 6) return false;
uint32_t value = 0;
for (char c : hex) {
int digit = c >= '0' && c <= '9' ? c - '0' : c >= 'A' && c <= 'F' ? c - 'A' + 10 : c >= 'a' && c <= 'f' ? c - 'a' + 10 : -1;
if (digit < 0) return false;
value = value << 4 | static_cast<uint32_t>(digit);
}
out = value;
return true;
}
// Next codepoint of UTF-8 text at `i` (advanced past it); 0 at the end. Broken bytes count as one.
inline uint32_t next_codepoint(const std::string &s, size_t &i) {
if (i >= s.size()) return 0;
unsigned char c = static_cast<unsigned char>(s[i]);
int extra = c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 0;
uint32_t cp = extra == 3 ? c & 0x07 : extra == 2 ? c & 0x0F : extra == 1 ? c & 0x1F : c;
++i;
for (int k = 0; k < extra && i < s.size() && (static_cast<unsigned char>(s[i]) & 0xC0) == 0x80; ++k, ++i)
cp = cp << 6 | (static_cast<unsigned char>(s[i]) & 0x3F);
return cp;
}
// 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.
inline std::string ago_text(int64_t then, int64_t now) {
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); };
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 < 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";
}
// "Mo 14 Sep"; 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"};
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];
}
// What you see between icon and value, between two items and after the name, from the height of
// the digits; the editor's barGaps() uses the same factors.
struct Gaps { int icon, item, name; };
inline int scaled(int cap, int tenths) { return (cap * tenths + 5) / 10; }
inline Gaps gaps(int cap) {
return {std::max(2, scaled(cap, 4)), std::max(6, (cap * 125 + 50) / 100), std::max(8, scaled(cap, 16))};
}
// Right-aligned placement of item widths (ink, gaps included) in `width` pixels beside a name of
// `name_natural` pixels. The name keeps at least min(natural, 35% of the width); items leave from
// the front until the rest fits. `x` is each item's left ink edge; `name_room` what the name may use.
struct Placement {
size_t first = 0;
std::array<int, MAX_ITEMS> x{};
int name_room = 0;
};
inline Placement place(const int *widths, size_t count, const Gaps &g, int width, int name_natural) {
Placement p;
count = std::min(count, MAX_ITEMS);
int min_name = std::min(name_natural, width * 35 / 100);
auto total = [&](size_t from) {
int sum = 0;
for (size_t i = from; i < count; ++i) sum += widths[i] + (i > from ? g.item : 0);
return sum;
};
while (p.first < count && total(p.first) + g.name + min_name > width) ++p.first;
int x = width - total(p.first);
for (size_t i = p.first; i < count; ++i) { p.x[i] = x; x += widths[i] + g.item; }
p.name_room = p.first < count ? p.x[p.first] - g.name : width;
return p;
}
} // namespace header_bar