Public Access
Interstellar: Gargantua with a time dilation clock (Earth today vs. Miller's planet), TARS with his settings and remarks about the house, the Endurance spinning at a solar-driven RPM while a Ranger docks, and the tesseract's bookshelf with the time in binary dust and the Message's first word in Morse on a watch. Your Name: comet Tiamat splitting over Itomori's lake, and kataware-doki, a countdown to sunset from sun.sun where two silhouettes stand on the crater rim during the twilight hour. HomeData gains sunset, sunrise and sun elevation (with an ISO time parser); wrap() and typed() move to el512_pages.h. Share Tech Mono 22 gets the full Latin set. All pages are in the renderer, checked with normal, busy and empty data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
216 lines
6.7 KiB
C++
216 lines
6.7 KiB
C++
// Shared pieces for the themed ESPHome pages (cyberpunk.cpp, konosuba.cpp): the
|
|
// Home Assistant values they show, and small drawing helpers.
|
|
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdarg>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "el512_helpers.h"
|
|
#include "esphome/components/display/display.h"
|
|
#include "esphome/core/time.h"
|
|
|
|
namespace esphome::el512 {
|
|
|
|
struct Reading {
|
|
std::string name;
|
|
float value{NAN};
|
|
};
|
|
|
|
// Home Assistant values shown on the pages; NAN or "" when unknown. Filled
|
|
// from YAML (see the cp_home global in el512-panel.yaml).
|
|
struct HomeData {
|
|
std::string weather; // condition, e.g. "partlycloudy"
|
|
float temp{NAN}; // °C
|
|
float humidity{NAN}; // %
|
|
float wind{NAN}; // km/h
|
|
float solar{NAN}; // W
|
|
float solar_today{NAN}; // Wh produced today
|
|
float house{NAN}; // W
|
|
float grid{NAN}; // W, > 0 = import
|
|
std::string agenda; // sensor.el512_agenda events: "day|time|title;..."
|
|
std::string message; // the panel's Message text
|
|
std::string printer; // 3D printer state (Moonraker: printing, paused, error, ...)
|
|
int64_t sunset{0}; // next sunset, Unix time (sun.sun next_setting); 0 = unknown
|
|
int64_t sunrise{0}; // next sunrise, Unix time (sun.sun next_rising); 0 = unknown
|
|
float sun_elevation{NAN}; // degrees above the horizon
|
|
Reading rooms[4];
|
|
|
|
// Today's solar power in 15-minute slots (highest value seen per slot; -1 =
|
|
// no data yet). Fed by record(), once a second from YAML.
|
|
static constexpr int SLOTS = 96;
|
|
float solar_slots[SLOTS];
|
|
int slots_day{-1};
|
|
|
|
HomeData() {
|
|
for (auto &s : this->solar_slots)
|
|
s = -1;
|
|
}
|
|
void record(ESPTime now) {
|
|
if (!now.is_valid())
|
|
return;
|
|
if (now.day_of_year != this->slots_day) {
|
|
this->slots_day = now.day_of_year;
|
|
for (auto &s : this->solar_slots)
|
|
s = -1;
|
|
}
|
|
float &slot = this->solar_slots[now.hour * 4 + now.minute / 15];
|
|
if (!std::isnan(this->solar))
|
|
slot = std::max(slot, this->solar);
|
|
}
|
|
};
|
|
|
|
// 1-bit sprite: rows of '#' (lit) and anything else (dark). Generated by the
|
|
// tools/*_sprites.py files.
|
|
struct Sprite {
|
|
uint8_t w, h;
|
|
const char *const *rows;
|
|
};
|
|
|
|
namespace fx {
|
|
|
|
// Integer hash for repeatable pseudo-random effects (lowbias32).
|
|
inline uint32_t hash(uint32_t x) {
|
|
x ^= x >> 16;
|
|
x *= 0x7feb352d;
|
|
x ^= x >> 15;
|
|
x *= 0x846ca68b;
|
|
x ^= x >> 16;
|
|
return x;
|
|
}
|
|
inline uint32_t hash(uint32_t a, uint32_t b) { return hash(a * 0x9e3779b9u ^ hash(b)); }
|
|
|
|
inline std::string str(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
|
|
inline std::string str(const char *fmt, ...) {
|
|
char buf[128];
|
|
va_list args;
|
|
va_start(args, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, args);
|
|
va_end(args);
|
|
return buf;
|
|
}
|
|
|
|
inline std::string upper(std::string s) {
|
|
for (auto &c : s)
|
|
c = (char) toupper((unsigned char) c);
|
|
return s;
|
|
}
|
|
|
|
// Days since 1970-01-01 for a civil date (Howard Hinnant's algorithm).
|
|
inline int64_t days_from_civil(int y, int m, int d) {
|
|
y -= m <= 2;
|
|
const int era = (y >= 0 ? y : y - 399) / 400;
|
|
const int yoe = y - era * 400;
|
|
const int doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;
|
|
const int doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
|
|
return (int64_t) era * 146097 + doe - 719468;
|
|
}
|
|
|
|
// "2026-10-19T16:32:10.123+00:00" (as Home Assistant sends datetimes) to Unix
|
|
// time; 0 if it doesn't parse.
|
|
inline int64_t parse_iso_utc(const std::string &s) {
|
|
int y, mo, d, h, mi, se;
|
|
if (sscanf(s.c_str(), "%d-%d-%dT%d:%d:%d", &y, &mo, &d, &h, &mi, &se) != 6)
|
|
return 0;
|
|
int64_t t = days_from_civil(y, mo, d) * 86400 + h * 3600 + mi * 60 + se;
|
|
const size_t p = s.find_first_of("+-", 19);
|
|
int oh, om;
|
|
if (p != std::string::npos && sscanf(s.c_str() + p + 1, "%d:%d", &oh, &om) == 2)
|
|
t -= (s[p] == '+' ? 1 : -1) * (int64_t) (oh * 3600 + om * 60);
|
|
return t;
|
|
}
|
|
|
|
inline std::string lower(std::string s) {
|
|
for (auto &c : s)
|
|
c = (char) tolower((unsigned char) c);
|
|
return s;
|
|
}
|
|
|
|
inline int text_width(display::Display &it, display::BaseFont *font, const char *text) {
|
|
int x, y, w, h;
|
|
it.get_text_bounds(0, 0, text, font, display::TextAlign::TOP_LEFT, &x, &y, &w, &h);
|
|
return w;
|
|
}
|
|
|
|
// Greedy word wrap into at most max_lines lines of `width` pixels; the last
|
|
// line ends in "..." when text is left over.
|
|
inline std::vector<std::string> wrap(display::Display &it, display::BaseFont *font, const std::string &text, int width,
|
|
size_t max_lines) {
|
|
std::vector<std::string> lines;
|
|
std::string line;
|
|
size_t i = 0;
|
|
while (i <= text.size()) {
|
|
size_t j = text.find(' ', i);
|
|
if (j == std::string::npos)
|
|
j = text.size();
|
|
const std::string word = text.substr(i, j - i);
|
|
const std::string joined = line.empty() ? word : line + " " + word;
|
|
if (!line.empty() && text_width(it, font, joined.c_str()) > width) {
|
|
lines.push_back(line);
|
|
line = word;
|
|
} else {
|
|
line = joined;
|
|
}
|
|
i = j + 1;
|
|
}
|
|
if (!line.empty())
|
|
lines.push_back(line);
|
|
if (lines.size() > max_lines) {
|
|
lines.resize(max_lines);
|
|
lines.back() += "...";
|
|
}
|
|
for (auto &l : lines)
|
|
l = fit(font, l, width);
|
|
return lines;
|
|
}
|
|
|
|
// The part of `s` typed so far at `cps` characters per second.
|
|
inline std::string typed(const std::string &s, uint32_t t, int cps) {
|
|
return s.substr(0, std::min<size_t>(s.size(), (size_t) t * cps / 1000));
|
|
}
|
|
|
|
// Draw a sprite (optionally only its first `rows` rows), scaled up in blocks.
|
|
inline void sprite(display::Display &it, const Sprite &s, int x, int y, int scale = 1, int rows = -1,
|
|
Color color = display::COLOR_ON) {
|
|
const int h = rows < 0 ? s.h : std::min<int>(rows, s.h);
|
|
for (int r = 0; r < h; r++) {
|
|
for (int c = 0; c < s.w; c++) {
|
|
if (s.rows[r][c] != '#')
|
|
continue;
|
|
if (scale == 1)
|
|
it.draw_pixel_at(x + c, y + r, color);
|
|
else
|
|
it.filled_rectangle(x + c * scale, y + r * scale, scale, scale, color);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Point on a circle; angle in degrees, 0 = up, clockwise.
|
|
inline void polar(int cx, int cy, float r, float a, int *x, int *y) {
|
|
constexpr float DEG = 3.14159265f / 180.0f;
|
|
*x = cx + (int) std::lround(r * std::sin(a * DEG));
|
|
*y = cy - (int) std::lround(r * std::cos(a * DEG));
|
|
}
|
|
|
|
// Circular arc from angle a0 to a1 (degrees, as polar()).
|
|
inline void arc(display::Display &it, int cx, int cy, float r, float a0, float a1) {
|
|
const int steps = std::max(2, (int) (std::fabs(a1 - a0) / 5));
|
|
int px, py;
|
|
polar(cx, cy, r, a0, &px, &py);
|
|
for (int i = 1; i <= steps; i++) {
|
|
int nx, ny;
|
|
polar(cx, cy, r, a0 + (a1 - a0) * i / steps, &nx, &ny);
|
|
it.line(px, py, nx, ny);
|
|
px = nx;
|
|
py = ny;
|
|
}
|
|
}
|
|
|
|
} // namespace fx
|
|
} // namespace esphome::el512
|