Files
homeassistant_espscreen/components/smart_display/history_view.h
T
Max Gramser 2bbb8f4e11 Give the history card axes, ranges and a finger readout (0.2.59)
A sensor, number, binary sensor, person or held switch opens a history card:
a line with round axis steps, clock times and the highest and lowest moment
for numbers, a timeline with the time in each state for on/off, zones and
statuses. The screen asks with esphome.screen_history for 1 hour, 24 hours or
a week; the app answers with one compact history message from the recorder's
statistics or the state changes. A finger on the graph reads the value and
time of that moment at the top while the graph stays as it is. The Guition
weather card starts below its round back button, and the README shows the new
cards and the light and weather cards as they look now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:20:51 +02:00

185 lines
8.2 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>
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.
inline std::string clock(int64_t epoch, int32_t offset, bool h24, bool weekday) {
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");
if (!weekday) return b;
return std::string(names[((days % 7) + 7) % 7]) + " " + 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.
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));
}
return b;
}
// A value as Home Assistant writes it: fixed decimals, commas between thousands, "%" 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];
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);
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;
text = (zero ? "" : sign) + whole + rest;
if (unit.empty()) return text;
if (unit == "%" || unit == "°") return text + unit;
return 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