125346d94e
From the end-to-end test on the bench boards and an independent review: - Units spaced as Home Assistant does (54% / 54 % in German and French, 18°, 21.5 °C), in the firmware and the app. - Grouping from five digits where the language does (1234 but 12.345 in Spanish, Polish, Portuguese): the layout message carries group_min and percent_space; an unknown number style no longer refuses a layout. - Automatic follows Home Assistant's own language for the clock and numbers, also without a translation (Swedish: 24 hours, 1 234,5; pt-BR: 1.234,5): regions.json from the CLDR for all 63 languages Home Assistant offers. - 12-hour clock: the top bar says 3:48 PM, the clock card 3:48 (not 03:48), Last runs follow the clock. - The climate setpoint font has the decimal comma; the value fonts have ², ³ and µ (m³ was a box). - Weather days: at most three letters without the dot (mié, sáb, czw); the screen took three bytes. - Texts fall back per key through the base language (pt-BR -> pt -> en) in the firmware and the app. - Each screen gets the app's words in the language its firmware speaks; firmware from before the languages gets English and its own letters, not boxes. - set_language re-reads what it writes (indentation, comments, quoted keys, CRLF, !include) and every build writes the language first; the 24-hour pin no longer hits a fresh install and waits for the old clock switches. - Tests: region and notation, plural parity with the C++ rules, set_language edge cases, the pin, PUT /api/language. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
184 lines
8.3 KiB
C++
184 lines
8.3 KiB
C++
#pragma once
|
|
// History on a detail card (firmware 0.2.51+): the pure parts. What a time, a duration or a value reads as, the
|
|
// smooth line through the averages, and which part of the line or which run of the timeline lies under a finger.
|
|
// The drawing is in runtime_tiles.h; tests/test_history_view.cpp covers this.
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
#include "screen_text.h"
|
|
|
|
namespace history_view {
|
|
constexpr int PARTS = 24;
|
|
|
|
// A run of the timeline: from `slot` to the next run's slot it shows `state` (an index into the legend, -1 without
|
|
// data). `begin`, `end` (seconds after the start of the range) and `seconds` are the real times of that state in it.
|
|
struct Run {
|
|
uint16_t slot = 0;
|
|
int8_t state = -1;
|
|
uint32_t begin = 0, end = 0, seconds = 0;
|
|
};
|
|
|
|
// 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, 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;
|
|
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, tr(hour < 12 ? txt::time_am : txt::time_pm));
|
|
if (!weekday) return 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.
|
|
inline std::string axis_clock(int64_t epoch, int32_t offset, bool h24) {
|
|
std::string text = clock(epoch, offset, h24, false);
|
|
if (h24) return text;
|
|
auto colon = text.find(':');
|
|
if (colon != std::string::npos && text.compare(colon, 3, ":00") == 0) text.erase(colon, 3);
|
|
return text;
|
|
}
|
|
|
|
// "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) {
|
|
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));
|
|
}
|
|
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, the language's separator between thousands and its decimal
|
|
// mark (screen.number: "1,234.5" in English, "1.234,5" in Dutch), the unit spaced as Home Assistant spaces it.
|
|
inline std::string number(float value, int decimals, const std::string &unit) {
|
|
if (!std::isfinite(value)) return "--";
|
|
char b[40];
|
|
snprintf(b, sizeof(b), "%.*f", std::clamp(decimals, 0, 4), value);
|
|
std::string text = b, sign;
|
|
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);
|
|
bool zero = whole.find_first_not_of('0') == std::string::npos && rest.find_first_not_of(".0") == std::string::npos;
|
|
text = (zero ? "" : sign) + screen_text::write_number(whole, rest.empty() ? std::string() : rest.substr(1));
|
|
return screen_text::with_unit(text, unit);
|
|
}
|
|
|
|
// Where part `i` of 24 is drawn and read: the middle of its stretch of the range, as a fraction of the width.
|
|
inline float part_middle(int i) { return (static_cast<float>(i) + 0.5f) / PARTS; }
|
|
|
|
// The part under a finger at `fraction` of the width. It can be a part without a value (a sensor that did not
|
|
// exist yet, a time it was unavailable): the card then says so instead of reading a value from another hour.
|
|
inline int part_at(float fraction) {
|
|
return std::clamp(static_cast<int>(std::floor(std::clamp(fraction, 0.0f, 1.0f) * PARTS)), 0, PARTS - 1);
|
|
}
|
|
|
|
// The run of the timeline under a finger: its index into `runs`, or -1.
|
|
inline int run_at(const std::vector<Run> &runs, uint16_t slots, float fraction) {
|
|
if (runs.empty() || !slots) return -1;
|
|
int slot = std::clamp(static_cast<int>(std::floor(std::clamp(fraction, 0.0f, 1.0f) * slots)), 0, slots - 1);
|
|
int found = -1;
|
|
for (size_t i = 0; i < runs.size(); ++i) {
|
|
if (runs[i].slot <= slot) found = static_cast<int>(i);
|
|
else break;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
// The slot where run `i` ends (the next run's start, or the end of the timeline).
|
|
inline uint16_t run_end(const std::vector<Run> &runs, uint16_t slots, size_t i) {
|
|
return i + 1 < runs.size() ? runs[i + 1].slot : slots;
|
|
}
|
|
|
|
// Puts a moment (x, y) on a line through points with rising x: the nearest point within `snap` (the same part of
|
|
// the range) takes its y, unless it is `keep` (a point that already holds a moment); otherwise the moment is added
|
|
// in its place. Returns the moment's index, or -1 when the points are full.
|
|
inline int through(float *xs, float *ys, unsigned &n, unsigned capacity, float x, float y, float snap, int keep = -1) {
|
|
int nearest = -1;
|
|
for (unsigned i = 0; i < n; ++i) {
|
|
const float distance = std::fabs(xs[i] - x);
|
|
if (static_cast<int>(i) != keep && distance <= snap && (nearest < 0 || distance < std::fabs(xs[nearest] - x))) nearest = static_cast<int>(i);
|
|
}
|
|
if (nearest >= 0) {
|
|
ys[nearest] = y;
|
|
return nearest;
|
|
}
|
|
if (n >= capacity) return -1;
|
|
unsigned at = n;
|
|
while (at > 0 && xs[at - 1] > x) {
|
|
xs[at] = xs[at - 1];
|
|
ys[at] = ys[at - 1];
|
|
--at;
|
|
}
|
|
xs[at] = x;
|
|
ys[at] = y;
|
|
++n;
|
|
return static_cast<int>(at);
|
|
}
|
|
|
|
// A smooth line through points with rising x that never overshoots them (monotone cubic, Fritsch-Carlson): between
|
|
// two points it stays between their values, so a step in a setpoint does not dip below its lowest value and the
|
|
// highest and lowest moment stay on the line. Writes `steps` points per stretch and the last point into `out`
|
|
// (anything with x and y), at most `capacity`; returns how many.
|
|
template<typename Point>
|
|
inline unsigned monotone(const float *xs, const float *ys, unsigned n, unsigned steps, Point *out, unsigned capacity) {
|
|
using Value = decltype(out[0].x);
|
|
auto put = [&](unsigned at, float x, float y) {
|
|
out[at].x = static_cast<Value>(std::lround(x));
|
|
out[at].y = static_cast<Value>(std::lround(y));
|
|
};
|
|
if (!capacity) return 0;
|
|
if (n < 2 || steps == 0) {
|
|
unsigned count = std::min(n, capacity);
|
|
for (unsigned i = 0; i < count; ++i) put(i, xs[i], ys[i]);
|
|
return count;
|
|
}
|
|
constexpr unsigned MOST = 64;
|
|
n = std::min(n, MOST);
|
|
float slope[MOST], tangent[MOST];
|
|
for (unsigned i = 0; i + 1 < n; ++i) {
|
|
const float dx = xs[i + 1] - xs[i];
|
|
slope[i] = dx > 0 ? (ys[i + 1] - ys[i]) / dx : 0.0f;
|
|
}
|
|
tangent[0] = slope[0];
|
|
tangent[n - 1] = slope[n - 2];
|
|
for (unsigned i = 1; i + 1 < n; ++i)
|
|
tangent[i] = slope[i - 1] * slope[i] <= 0 ? 0.0f : (slope[i - 1] + slope[i]) / 2;
|
|
for (unsigned i = 0; i + 1 < n; ++i) {
|
|
if (slope[i] == 0) { tangent[i] = tangent[i + 1] = 0; continue; }
|
|
const float a = tangent[i] / slope[i], b = tangent[i + 1] / slope[i], sum = a * a + b * b;
|
|
if (sum > 9) {
|
|
const float scale = 3 / std::sqrt(sum);
|
|
tangent[i] = scale * a * slope[i];
|
|
tangent[i + 1] = scale * b * slope[i];
|
|
}
|
|
}
|
|
unsigned count = 0;
|
|
for (unsigned i = 0; i + 1 < n && count + 1 < capacity; ++i) {
|
|
const float h = xs[i + 1] - xs[i];
|
|
for (unsigned s = 0; s < steps && count + 1 < capacity; ++s) {
|
|
const float t = static_cast<float>(s) / steps, t2 = t * t, t3 = t2 * t;
|
|
const float y = (2 * t3 - 3 * t2 + 1) * ys[i] + (t3 - 2 * t2 + t) * h * tangent[i] + (-2 * t3 + 3 * t2) * ys[i + 1] + (t3 - t2) * h * tangent[i + 1];
|
|
put(count++, xs[i] + t * h, y);
|
|
}
|
|
}
|
|
put(count++, xs[n - 1], ys[n - 1]);
|
|
return count;
|
|
}
|
|
} // namespace history_view
|