// Shared pieces for the themed ESPHome pages (cyberpunk.cpp, konosuba.cpp): the // Home Assistant values they show, and small drawing helpers. #pragma once #include #include #include #include #include #include #include #include #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 wrap(display::Display &it, display::BaseFont *font, const std::string &text, int width, size_t max_lines) { std::vector 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(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(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