Files
mangavalkandClaude Opus 5.5 f46f7ab6c5 Add Interstellar and Your Name pages
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>
2026-10-19 18:59:00 +02:00

943 lines
36 KiB
YAML

# Planar EL512.256-H (512x256 amber EL) on a Wemos D1 mini ESP32, as a
# Home Assistant information display.
#
# Needs: secrets.yaml (wifi_ssid, wifi_password, el512_api_key, el512_ota_password)
# and the helper sensors from home_assistant/packages/el512_panel.yaml.
# Wiring: see the README of https://mangavalk.com/mangavalk/planar-EL512.256-H
substitutions:
name: el512-panel
friendly_name: EL512 Panel
weather: weather.knmi_home
envoy: sensor.envoy_122225067403
printer: sensor.mainsailos_current_print_state # Moonraker; "paused"/"error" = red alert
esphome:
name: ${name}
friendly_name: ${friendly_name}
on_boot:
- lambda: |-
id(cp_fonts) = {id(f_cp_title), id(f_cp_label), id(f_cp_value), id(f_cp_huge), id(f_cp_mono), id(f_cp_code)};
id(ks_fonts) = {id(f_ks_title), id(f_ks_big), id(f_ks_text), id(f_ks_pixel), id(f_ks_pixel_l), id(f_ks_jp),
id(f_ks_jp_big), id(icons_l)};
id(sg_fonts) = {id(f_sg_nixie), id(f_sg_title), id(f_cp_mono), id(f_cp_code), id(f_ks_pixel), id(f_sg_jp)};
id(st_fonts) = {id(f_st_small), id(f_st_medium), id(f_st_large), id(f_ks_text), id(f_ks_pixel)};
id(is_fonts) = {id(f_is_title), id(f_is_large), id(f_cp_mono), id(f_cp_code)};
id(yn_fonts) = {id(f_yn_jp), id(f_yn_hand), id(f_yn_clock), id(f_cp_mono)};
esp32:
board: wemos_d1_mini32
framework:
type: esp-idf
external_components:
- source:
type: git
url: https://mangavalk.com/mangavalk/planar-EL512.256-H.git
ref: main
components: [el512]
refresh: 0s
logger:
api:
encryption:
key: !secret el512_api_key
ota:
- platform: esphome
password: !secret el512_ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: ${name}
time:
- platform: homeassistant
id: ha_time
timezone: Europe/Amsterdam
globals:
- id: rotation_elapsed
type: int
initial_value: "0"
- id: np_updated
type: uint32_t
initial_value: "0"
- id: plex_updated
type: uint32_t
initial_value: "0"
# Themed pages (Cyberpunk, KonoSuba, Steins;Gate, Star Trek, Interstellar, Your Name): their fonts and the home data they show.
- id: cp_fonts
type: el512::cyber::Fonts
- id: ks_fonts
type: el512::konosuba::Fonts
- id: sg_fonts
type: el512::steins::Fonts
- id: st_fonts
type: el512::trek::Fonts
- id: is_fonts
type: el512::interstellar::Fonts
- id: yn_fonts
type: el512::yourname::Fonts
- id: cp_home
type: el512::HomeData
# --- Fonts -------------------------------------------------------------------
font:
- file: "gfonts://Roboto"
id: f_s
size: 16
glyphsets: [GF_Latin_Core]
- file: "gfonts://Roboto"
id: f_m
size: 22
glyphsets: [GF_Latin_Core]
- file: "gfonts://Roboto"
id: f_l
size: 32
glyphsets: [GF_Latin_Core]
- file: "gfonts://Roboto@500"
id: f_big
size: 48
glyphs: " 0123456789.,:-+%°CWkhMbits/"
- file: "gfonts://Roboto@300"
id: f_clock
size: 120
glyphs: "0123456789:"
- file: &mdi
type: web
url: https://github.com/Templarian/MaterialDesign-Webfont/raw/master/fonts/materialdesignicons-webfont.ttf
id: icons_xl
size: 96
glyphs: &weather_icons
- "\U000F0594" # weather-night
- "\U000F0590" # weather-cloudy
- "\U000F0591" # weather-fog
- "\U000F0592" # weather-hail
- "\U000F0593" # weather-lightning
- "\U000F067E" # weather-lightning-rainy
- "\U000F0595" # weather-partly-cloudy
- "\U000F0596" # weather-pouring
- "\U000F0597" # weather-rainy
- "\U000F0598" # weather-snowy
- "\U000F067F" # weather-snowy-rainy
- "\U000F0599" # weather-sunny
- "\U000F059D" # weather-windy
- "\U000F059E" # weather-windy-variant
- "\U000F05D6" # alert-circle-outline
- file: *mdi
id: icons_l
size: 56
glyphs: *weather_icons
- file: *mdi
id: icons_m
size: 32
glyphs:
- "\U000F0A72" # solar-power
- "\U000F0D3E" # transmission-tower
- "\U000F06A1" # home-outline
- "\U000F06BA" # plex
- "\U000F0E17" # calendar-month
- "\U000F040A" # play
- "\U000F0387" # music-note
- "\U000F048D" # server-network
- "\U000F048E" # server-network-off
- "\U000F06B0" # update
- "\U000F054B" # umbrella-outline
- "\U000F072E" # arrow-down-bold
- "\U000F0737" # arrow-up-bold
- file: *mdi
id: icons_s
size: 18
glyphs:
- "\U000F054B" # umbrella-outline
- "\U000F040A" # play
- "\U000F03E4" # pause
# Cyberpunk pages
- file: "gfonts://Orbitron@700"
id: f_cp_title
size: 18
glyphs: " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/_-.:>"
- file: "gfonts://Rajdhani@600"
id: f_cp_label
size: 18
glyphsets: [GF_Latin_Core]
- file: "gfonts://Rajdhani@700"
id: f_cp_value
size: 32
glyphs: " 0123456789.,:-+%°CkWh"
- file: "gfonts://Rajdhani@700"
id: f_cp_huge
size: 104
glyphs: "0123456789:"
- file: "gfonts://Share Tech Mono"
id: f_cp_mono
size: 14
glyphsets: [GF_Latin_Core]
- file: "gfonts://Share Tech Mono"
id: f_cp_code
size: 22
glyphsets: [GF_Latin_Core]
# KonoSuba pages
- file: "gfonts://Cinzel@700"
id: f_ks_title
size: 22
glyphs: " ABCDEFGHIJKLMNOPQRSTUVWXYZ'!"
- file: "gfonts://Cinzel Decorative@900"
id: f_ks_big
size: 44
glyphs: "EXPLOSIN!"
- file: "gfonts://Nunito@700"
id: f_ks_text
size: 16
glyphsets: [GF_Latin_Core]
- file: "gfonts://Press Start 2P"
id: f_ks_pixel
size: 8
glyphsets: [GF_Latin_Core]
- file: "gfonts://Press Start 2P"
id: f_ks_pixel_l
size: 16
glyphs: " 0123456789:!ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- file: "gfonts://Noto Sans JP@700"
id: f_ks_jp
size: 20
glyphs: "冒険者ギルドカー戦闘" # each character once
- file: "gfonts://Dela Gothic One"
id: f_ks_jp_big
size: 40
glyphs: "爆裂魔法"
# Steins;Gate pages (also use f_cp_mono, f_cp_code and f_ks_pixel)
- file: "gfonts://Nixie One"
id: f_sg_nixie
size: 90
glyphs: "0123456789."
- file: "gfonts://Special Elite"
id: f_sg_title
size: 20
glyphsets: [GF_Latin_Core]
- file: "gfonts://Noto Sans JP@700"
id: f_sg_jp
size: 18
glyphs: "未来ガジェット研究所世界線"
# Star Trek pages: Antonio stands in for the LCARS typeface (also use
# f_ks_text and f_ks_pixel)
- file: "gfonts://Antonio@600"
id: f_st_small
size: 16
glyphsets: [GF_Latin_Core]
- file: "gfonts://Antonio@600"
id: f_st_medium
size: 26
glyphsets: [GF_Latin_Core]
- file: "gfonts://Antonio@700"
id: f_st_large
size: 60
glyphs: " 0123456789:ADELRT"
# Interstellar pages (also use f_cp_mono and f_cp_code)
- file: "gfonts://Michroma"
id: f_is_title
size: 12
glyphsets: [GF_Latin_Core]
- file: "gfonts://Michroma"
id: f_is_large
size: 28
glyphs: " 0123456789:.ARST"
# Your Name pages (also use f_cp_mono)
- file: "gfonts://Yuji Syuku"
id: f_yn_jp
size: 26
glyphs: "君の名は。かたわれ時"
- file: "gfonts://Caveat@700"
id: f_yn_hand
size: 22
glyphsets: [GF_Latin_Core]
- file: "gfonts://Cormorant Garamond@600"
id: f_yn_clock
size: 48
glyphs: "0123456789:"
# --- Data from Home Assistant -------------------------------------------------
text_sensor:
- platform: homeassistant
id: weather_cond
entity_id: ${weather}
- platform: homeassistant
id: forecast_days
entity_id: sensor.el512_forecast
attribute: days
- platform: homeassistant
id: agenda_events
entity_id: sensor.el512_agenda
attribute: events
- platform: homeassistant
id: np_state
entity_id: sensor.el512_now_playing
- platform: homeassistant
id: np_info
entity_id: sensor.el512_now_playing
attribute: info
on_value:
- lambda: id(np_updated) = millis();
- platform: homeassistant
id: plex_server
entity_id: sensor.mangavalkserver
- platform: homeassistant
id: plex_streams
entity_id: sensor.el512_plex_streams
attribute: streams
on_value:
- lambda: id(plex_updated) = millis();
- platform: homeassistant
id: printer_state
entity_id: ${printer}
- platform: homeassistant
id: sun_setting
entity_id: sun.sun
attribute: next_setting
- platform: homeassistant
id: sun_rising
entity_id: sun.sun
attribute: next_rising
- platform: homeassistant
id: plex_update
entity_id: update.plex_media_server_mangavalkserver
- platform: homeassistant
id: plex_version
entity_id: update.plex_media_server_mangavalkserver
attribute: installed_version
sensor:
- platform: homeassistant
id: weather_temp
entity_id: ${weather}
attribute: temperature
- platform: homeassistant
id: weather_hum
entity_id: ${weather}
attribute: humidity
- platform: homeassistant
id: weather_wind
entity_id: ${weather}
attribute: wind_speed
# Plex / Tautulli
- platform: homeassistant
id: plex_watching
entity_id: sensor.mangavalkserver
- platform: homeassistant
id: tautulli_bandwidth
entity_id: sensor.tautulli_total_bandwidth
- platform: homeassistant
id: sun_elev
entity_id: sun.sun
attribute: elevation
# Solar (Enphase Envoy) and grid
- platform: homeassistant
id: solar_power
entity_id: ${envoy}_current_power_production
- platform: homeassistant
id: solar_today
entity_id: ${envoy}_today_s_energy_production
- platform: homeassistant
id: house_power
entity_id: ${envoy}_current_power_consumption
- platform: homeassistant
id: grid_import
entity_id: sensor.grid_import_power
- platform: homeassistant
id: grid_export
entity_id: sensor.grid_export_power
# Room temperatures (thermostat zones)
- platform: homeassistant
id: t_woonkamer
entity_id: climate.woonkamer
attribute: current_temperature
- platform: homeassistant
id: t_voorkamer
entity_id: climate.voorkamer
attribute: current_temperature
- platform: homeassistant
id: t_gang
entity_id: climate.vloerverwarm
attribute: current_temperature
- platform: homeassistant
id: t_patrick
entity_id: climate.patrick
attribute: current_temperature
- platform: homeassistant
id: t_badkamer
entity_id: climate.badkamer
attribute: current_temperature
- platform: homeassistant
id: t_stans_paul
entity_id: climate.stans_paul
attribute: current_temperature
- platform: homeassistant
id: t_vloer
entity_id: climate.vloererwarmin
attribute: current_temperature
- platform: homeassistant
id: t_huis
entity_id: climate.vigilantestraat
attribute: current_temperature
graph:
- id: solar_graph
sensor: solar_power
duration: 16h
width: 496
height: 118
x_grid: 2h
y_grid: 1000
min_value: 0
# --- Controls in Home Assistant ------------------------------------------------
switch:
- platform: template
id: sw_display
name: Display
icon: mdi:monitor
restore_mode: RESTORE_DEFAULT_ON
lambda: return id(panel).is_enabled();
turn_on_action:
- lambda: id(panel).set_enabled(true);
turn_off_action:
- lambda: id(panel).set_enabled(false);
- platform: template
id: sw_low_power
name: Low power mode
icon: mdi:leaf
restore_mode: RESTORE_DEFAULT_OFF
lambda: return id(panel).is_low_power();
turn_on_action:
- lambda: id(panel).set_low_power(true);
turn_off_action:
- lambda: id(panel).set_low_power(false);
- platform: template
id: sw_pause
name: Pause page rotation
icon: mdi:pause-circle-outline
optimistic: true
restore_mode: RESTORE_DEFAULT_OFF
number:
- platform: template
id: page_interval
name: Page interval
icon: mdi:timer-outline
unit_of_measurement: s
min_value: 5
max_value: 300
step: 5
initial_value: 20
optimistic: true
restore_value: true
text:
- platform: template
id: panel_message
name: Message
icon: mdi:message-text-outline
mode: text
max_length: 100
optimistic: true
restore_value: true
initial_value: ""
on_value:
- component.update: panel
select:
- platform: template
id: page_select
name: Page
icon: mdi:book-open-page-variant-outline
options:
- Clock
- Forecast
- Agenda
- Dashboard
- Now playing
- Plex
- Solar
- Still Alive
- Cyber HUD
- Cyber Breach
- Cyber Scan
- Cyber Split
- KonoSuba Explosion
- KonoSuba Guild
- KonoSuba Card
- KonoSuba Battle
- Divergence Meter
- D-Mail
- Gadget Lab
- World Lines
- LCARS Status
- Duty Roster
- Warp Core
- Captain's Log
- Gargantua
- TARS
- Endurance
- Tesseract
- Comet Tiamat
- Kataware-doki
initial_option: Clock
set_action:
- lambda: |-
id(rotation_elapsed) = 0;
if (x == "Still Alive") { id(panel).play_still_alive(); id(panel).update(); return; }
display::DisplayPage *p = id(page_clock);
if (x == "Forecast") p = id(page_forecast);
else if (x == "Agenda") p = id(page_agenda);
else if (x == "Dashboard") p = id(page_dashboard);
else if (x == "Now playing") p = id(page_media);
else if (x == "Plex") p = id(page_plex);
else if (x == "Solar") p = id(page_solar);
else if (x == "Cyber HUD") p = id(page_cyber_hud);
else if (x == "Cyber Breach") p = id(page_cyber_breach);
else if (x == "Cyber Scan") p = id(page_cyber_scan);
else if (x == "Cyber Split") p = id(page_cyber_split);
else if (x == "KonoSuba Explosion") p = id(page_ks_explosion);
else if (x == "KonoSuba Guild") p = id(page_ks_guild);
else if (x == "KonoSuba Card") p = id(page_ks_card);
else if (x == "KonoSuba Battle") p = id(page_ks_battle);
else if (x == "Divergence Meter") p = id(page_sg_meter);
else if (x == "D-Mail") p = id(page_sg_dmail);
else if (x == "Gadget Lab") p = id(page_sg_lab);
else if (x == "World Lines") p = id(page_sg_world);
else if (x == "LCARS Status") p = id(page_st_lcars);
else if (x == "Duty Roster") p = id(page_st_roster);
else if (x == "Warp Core") p = id(page_st_warp);
else if (x == "Captain's Log") p = id(page_st_log);
else if (x == "Gargantua") p = id(page_is_gargantua);
else if (x == "TARS") p = id(page_is_tars);
else if (x == "Endurance") p = id(page_is_endurance);
else if (x == "Tesseract") p = id(page_is_tesseract);
else if (x == "Comet Tiamat") p = id(page_yn_comet);
else if (x == "Kataware-doki") p = id(page_yn_twilight);
id(panel).show_page(p);
id(panel).update();
button:
- platform: template
name: Play Still Alive
icon: mdi:cake-variant-outline
on_press:
- lambda: |-
id(panel).play_still_alive();
id(panel).update();
# Rotate pages in a random order, reshuffled every hour (Still Alive included:
# rotation waits while it plays); Now playing is skipped when idle.
interval:
- interval: 1s
then:
- lambda: |-
// Red alert (the 3D printer paused or failed): jump to LCARS once,
// then hold the rotation until the printer carries on.
static bool was_alert = false;
const bool alert = el512::trek::red_alert(id(cp_home));
if (alert && !was_alert) { id(panel).show_page(id(page_st_lcars)); id(panel).update(); }
was_alert = alert;
if (alert) { id(rotation_elapsed) = 0; return; }
if (id(sw_pause).state || id(panel).is_still_alive_playing()) { id(rotation_elapsed) = 0; return; }
if (++id(rotation_elapsed) < (int) id(page_interval).state) return;
id(rotation_elapsed) = 0;
static std::vector<display::DisplayPage *> order;
static size_t pos = 0;
static int shuffled_hour = -1;
auto now = id(ha_time).now();
const int hour = now.is_valid() ? now.hour : (int) (millis() / 3600000UL);
if (order.empty() || hour != shuffled_hour) {
shuffled_hour = hour;
order = {id(page_clock), id(page_forecast), id(page_agenda), id(page_dashboard), id(page_media),
id(page_plex), id(page_solar), id(page_cyber_hud), id(page_cyber_breach), id(page_cyber_scan),
id(page_cyber_split), id(page_ks_explosion), id(page_ks_guild), id(page_ks_card),
id(page_ks_battle), id(page_sg_meter), id(page_sg_dmail), id(page_sg_lab), id(page_sg_world),
id(page_st_lcars), id(page_st_roster), id(page_st_warp), id(page_st_log),
id(page_is_gargantua), id(page_is_tars), id(page_is_endurance), id(page_is_tesseract),
id(page_yn_comet), id(page_yn_twilight), id(page_still_alive)};
for (size_t i = order.size() - 1; i > 0; i--)
std::swap(order[i], order[random_uint32() % (i + 1)]);
pos = order.size() - 1; // the next step starts at the front
}
const std::string &np = id(np_state).state;
for (size_t n = 0; n < order.size(); n++) {
pos = (pos + 1) % order.size();
display::DisplayPage *p = order[pos];
if (p == id(page_media) && (np == "idle" || np.empty())) continue;
if (p == id(panel).get_active_page()) continue; // not twice in a row across a reshuffle
id(panel).show_page(p);
break;
}
id(panel).update();
# Home data for the themed pages.
- interval: 1s
then:
- lambda: |-
auto &d = id(cp_home);
d.weather = id(weather_cond).state;
d.temp = id(weather_temp).state;
d.humidity = id(weather_hum).state;
d.wind = id(weather_wind).state;
d.solar = id(solar_power).state;
d.solar_today = id(solar_today).state;
d.agenda = id(agenda_events).state;
d.message = id(panel_message).state;
d.printer = id(printer_state).state;
d.sunset = el512::fx::parse_iso_utc(id(sun_setting).state);
d.sunrise = el512::fx::parse_iso_utc(id(sun_rising).state);
d.sun_elevation = id(sun_elev).state;
d.house = id(house_power).state;
d.grid = id(grid_import).state - id(grid_export).state;
d.rooms[0] = {"Woonkamer", id(t_woonkamer).state};
d.rooms[1] = {"Voorkamer", id(t_voorkamer).state};
d.rooms[2] = {"Patrick", id(t_patrick).state};
d.rooms[3] = {"Badkamer", id(t_badkamer).state};
d.record(id(ha_time).now());
# --- The panel and its pages ----------------------------------------------------
display:
- platform: el512
id: panel
enable_pin: GPIO25
low_power_pin: GPIO26
update_interval: 1s
still_alive_page: page_still_alive
# Redrawn every animation_interval (~15 fps) while shown.
animated_pages: [page_cyber_hud, page_cyber_breach, page_cyber_scan, page_cyber_split,
page_ks_explosion, page_ks_guild, page_ks_card, page_ks_battle,
page_sg_meter, page_sg_dmail, page_sg_lab, page_sg_world,
page_st_lcars, page_st_roster, page_st_warp, page_st_log,
page_is_gargantua, page_is_tars, page_is_endurance, page_is_tesseract,
page_yn_comet, page_yn_twilight]
animation_interval: 66ms
on_page_change:
- then:
- lambda: |-
const char *name = "Clock";
if (to == id(page_forecast)) name = "Forecast";
else if (to == id(page_agenda)) name = "Agenda";
else if (to == id(page_dashboard)) name = "Dashboard";
else if (to == id(page_media)) name = "Now playing";
else if (to == id(page_plex)) name = "Plex";
else if (to == id(page_solar)) name = "Solar";
else if (to == id(page_still_alive)) name = "Still Alive";
else if (to == id(page_cyber_hud)) name = "Cyber HUD";
else if (to == id(page_cyber_breach)) name = "Cyber Breach";
else if (to == id(page_cyber_scan)) name = "Cyber Scan";
else if (to == id(page_cyber_split)) name = "Cyber Split";
else if (to == id(page_ks_explosion)) name = "KonoSuba Explosion";
else if (to == id(page_ks_guild)) name = "KonoSuba Guild";
else if (to == id(page_ks_card)) name = "KonoSuba Card";
else if (to == id(page_ks_battle)) name = "KonoSuba Battle";
else if (to == id(page_sg_meter)) name = "Divergence Meter";
else if (to == id(page_sg_dmail)) name = "D-Mail";
else if (to == id(page_sg_lab)) name = "Gadget Lab";
else if (to == id(page_sg_world)) name = "World Lines";
else if (to == id(page_st_lcars)) name = "LCARS Status";
else if (to == id(page_st_roster)) name = "Duty Roster";
else if (to == id(page_st_warp)) name = "Warp Core";
else if (to == id(page_st_log)) name = "Captain's Log";
else if (to == id(page_is_gargantua)) name = "Gargantua";
else if (to == id(page_is_tars)) name = "TARS";
else if (to == id(page_is_endurance)) name = "Endurance";
else if (to == id(page_is_tesseract)) name = "Tesseract";
else if (to == id(page_yn_comet)) name = "Comet Tiamat";
else if (to == id(page_yn_twilight)) name = "Kataware-doki";
if (id(page_select).current_option() != name) id(page_select).publish_state(name);
pages:
# 1. Clock, date and current weather
- id: page_clock
lambda: |-
auto now = id(ha_time).now();
if (now.is_valid()) {
it.strftime(150, 100, id(f_clock), TextAlign::CENTER, "%H:%M", now);
it.strftime(150, 192, id(f_m), TextAlign::CENTER, "%A %d %B", now);
}
it.vertical_line(302, 24, 208);
const std::string &cond = id(weather_cond).state;
it.print(408, 64, id(icons_xl), TextAlign::CENTER, el512::weather_icon(cond));
if (!std::isnan(id(weather_temp).state))
it.printf(408, 140, id(f_big), TextAlign::CENTER, "%.1f°", id(weather_temp).state);
it.print(408, 184, id(f_m), TextAlign::CENTER, el512::weather_text(cond).c_str());
if (!std::isnan(id(weather_hum).state))
it.printf(408, 214, id(f_s), TextAlign::CENTER, "%.0f%% humidity %.0f km/h",
id(weather_hum).state, id(weather_wind).state);
# 2. Forecast (one column per day)
- id: page_forecast
lambda: |-
auto days = el512::split(id(forecast_days).state, ';');
const int n = std::min<int>(days.size(), 7);
if (n == 0) {
it.print(256, 128, id(f_m), TextAlign::CENTER, "No forecast (sensor.el512_forecast)");
return;
}
const int w = 512 / n;
for (int i = 0; i < n; i++) {
const int x = w * i + w / 2;
if (i > 0) it.vertical_line(w * i, 16, 224);
it.print(x, 10, id(f_m), TextAlign::TOP_CENTER, el512::field(days[i], 0).c_str());
it.print(x, 98, id(icons_l), TextAlign::CENTER, el512::weather_icon(el512::field(days[i], 1)));
it.printf(x, 140, id(f_l), TextAlign::TOP_CENTER, "%s°", el512::field(days[i], 2).c_str());
it.printf(x, 180, id(f_m), TextAlign::TOP_CENTER, "%s°", el512::field(days[i], 3).c_str());
it.print(x - 4, 224, id(icons_s), TextAlign::TOP_RIGHT, "\U000F054B");
it.printf(x - 2, 222, id(f_s), TextAlign::TOP_LEFT, "%s%%", el512::field(days[i], 4).c_str());
}
# 3. Calendar agenda
- id: page_agenda
lambda: |-
it.print(8, 4, id(icons_m), TextAlign::TOP_LEFT, "\U000F0E17");
it.print(48, 8, id(f_m), TextAlign::TOP_LEFT, "Agenda");
auto now = id(ha_time).now();
if (now.is_valid()) it.strftime(504, 10, id(f_s), TextAlign::TOP_RIGHT, "%A %d %B", now);
it.horizontal_line(0, 40, 512);
auto events = el512::split(id(agenda_events).state, ';');
if (events.empty()) {
it.print(256, 140, id(f_m), TextAlign::CENTER, "Nothing planned");
return;
}
int y = 48;
std::string last_day;
for (auto &e : events) {
if (y > 256 - 24) break;
const std::string day = el512::field(e, 0), time = el512::field(e, 1);
if (day != last_day) {
if (!last_day.empty()) it.horizontal_line(8, y - 2, 496);
it.print(8, y, id(f_m), TextAlign::TOP_LEFT, day.c_str());
last_day = day;
}
it.print(96, y, id(f_m), TextAlign::TOP_LEFT, time.empty() ? "all day" : time.c_str());
it.print(176, y, id(f_m), TextAlign::TOP_LEFT, el512::fit(id(f_m), el512::field(e, 2), 330).c_str());
y += 26;
}
# 4. Sensor dashboard and message line
- id: page_dashboard
lambda: |-
struct Tile { const char *name; float value; };
const Tile tiles[9] = {
{"Outside", id(weather_temp).state}, {"Woonkamer", id(t_woonkamer).state},
{"Voorkamer", id(t_voorkamer).state}, {"Gang", id(t_gang).state},
{"Patrick", id(t_patrick).state}, {"Badkamer", id(t_badkamer).state},
{"Stans & Paul", id(t_stans_paul).state}, {"Vloerverwarming", id(t_vloer).state},
{"Huis", id(t_huis).state},
};
for (int i = 0; i < 9; i++) {
const int x = (i % 3) * 171, y = (i / 3) * 68;
it.print(x + 10, y + 6, id(f_s), TextAlign::TOP_LEFT, tiles[i].name);
if (std::isnan(tiles[i].value))
it.print(x + 10, y + 24, id(f_l), TextAlign::TOP_LEFT, "--");
else
it.printf(x + 10, y + 24, id(f_l), TextAlign::TOP_LEFT, "%.1f°", tiles[i].value);
}
it.vertical_line(170, 4, 196);
it.vertical_line(341, 4, 196);
it.horizontal_line(0, 210, 512);
const std::string &msg = id(panel_message).state;
if (!msg.empty()) {
it.print(256, 234, id(f_m), TextAlign::CENTER, el512::fit(id(f_m), msg, 496).c_str());
} else {
const float net = id(grid_import).state - id(grid_export).state;
it.printf(256, 234, id(f_s), TextAlign::CENTER, "Solar %s House %s Grid %s",
el512::watts(id(solar_power).state).c_str(), el512::watts(id(house_power).state).c_str(),
el512::watts(net).c_str());
}
# 5. Now playing (whichever media player plays)
- id: page_media
lambda: |-
auto f = el512::split(id(np_info).state, '|'); // title|artist|album|player|pos|dur
if (f.size() < 6 || id(np_state).state == "idle") {
it.print(256, 118, id(icons_m), TextAlign::CENTER, "\U000F0387");
it.print(256, 160, id(f_m), TextAlign::CENTER, "Nothing playing");
return;
}
it.print(8, 6, id(icons_m), TextAlign::TOP_LEFT, "\U000F040A");
it.print(48, 12, id(f_s), TextAlign::TOP_LEFT, el512::fit(id(f_s), f[3], 450).c_str());
it.print(8, 56, id(f_l), TextAlign::TOP_LEFT, el512::fit(id(f_l), f[0], 496).c_str());
it.print(8, 104, id(f_m), TextAlign::TOP_LEFT, el512::fit(id(f_m), f[1], 496).c_str());
it.print(8, 136, id(f_s), TextAlign::TOP_LEFT, el512::fit(id(f_s), f[2], 496).c_str());
const int dur = atoi(f[5].c_str());
int pos = atoi(f[4].c_str()) + (int) ((millis() - id(np_updated)) / 1000);
if (dur > 0) {
pos = std::min(pos, dur);
it.rectangle(8, 190, 496, 14);
it.filled_rectangle(8, 190, 496 * pos / dur, 14);
it.print(8, 212, id(f_s), TextAlign::TOP_LEFT, el512::mmss(pos).c_str());
it.print(504, 212, id(f_s), TextAlign::TOP_RIGHT, el512::mmss(dur).c_str());
}
# 6. Plex: streams, server status (Plex + Tautulli integrations)
- id: page_plex
lambda: |-
const bool online = id(plex_server).state != "unavailable" && !id(plex_server).state.empty();
it.print(8, 4, id(icons_m), TextAlign::TOP_LEFT, "\U000F06BA");
it.print(48, 8, id(f_m), TextAlign::TOP_LEFT, "Plex");
it.print(504, 4, id(icons_m), TextAlign::TOP_RIGHT, online ? "\U000F048D" : "\U000F048E");
std::string ver = id(plex_version).state;
ver = ver.substr(0, ver.find('-'));
it.printf(462, 10, id(f_s), TextAlign::TOP_RIGHT, "%s%s%s", online ? "online" : "offline",
ver.empty() ? "" : " v", ver.c_str());
if (id(plex_update).state == "on")
it.print(300, 10, id(f_s), TextAlign::TOP_RIGHT, "update available");
it.horizontal_line(0, 40, 512);
// Left: stream count and bandwidth
const float n = id(plex_watching).state;
it.printf(70, 100, id(f_big), TextAlign::CENTER, "%.0f", std::isnan(n) ? 0.0f : n);
it.print(70, 134, id(f_s), TextAlign::TOP_CENTER, n == 1 ? "stream" : "streams");
if (!std::isnan(id(tautulli_bandwidth).state))
it.printf(70, 170, id(f_m), TextAlign::TOP_CENTER, "%.1f Mbit/s", id(tautulli_bandwidth).state / 1000.0f);
it.vertical_line(140, 48, 200);
// Right: who is watching what
auto streams = el512::split(id(plex_streams).state, ';');
if (streams.empty()) {
it.print(326, 140, id(f_m), TextAlign::CENTER, online ? "Nobody is watching" : "Server offline");
return;
}
// Per stream: play/pause, who, time, title and a progress bar
// (who|title|state|pos|dur; the server-sensor fallback has no state).
int y = 50;
for (auto &s : streams) {
if (y > 256 - 48) break;
auto f = el512::split(s, '|');
f.resize(5);
const bool playing = f[2] == "playing", paused = f[2] == "paused";
const int dur = atoi(f[4].c_str());
int pos = atoi(f[3].c_str());
if (playing) pos += (int) ((millis() - id(plex_updated)) / 1000);
pos = std::min(pos, dur);
int x = 152;
if (playing || paused) {
it.print(150, y - 1, id(icons_s), TextAlign::TOP_LEFT, paused ? "\U000F03E4" : "\U000F040A");
x = 172;
}
std::string when;
if (dur > 0) when = el512::mmss(pos) + " / " + el512::mmss(dur);
int tw = 0, tx, ty, th;
if (!when.empty()) {
it.get_text_bounds(0, 0, when.c_str(), id(f_s), TextAlign::TOP_LEFT, &tx, &ty, &tw, &th);
it.print(504, y, id(f_s), TextAlign::TOP_RIGHT, when.c_str());
}
it.print(x, y, id(f_s), TextAlign::TOP_LEFT, el512::fit(id(f_s), f[0], 504 - x - tw - 10).c_str());
it.print(152, y + 17, id(f_m), TextAlign::TOP_LEFT, el512::fit(id(f_m), f[1], 352).c_str());
if (dur > 0) {
const int w = 352 * pos / dur;
if (paused) {
// Paused: hollow bar with a dotted fill.
it.rectangle(152, y + 42, 352, 5);
for (int px = 154; px < 152 + w; px += 2) it.draw_pixel_at(px, y + 44);
} else {
it.horizontal_line(152, y + 44, 352);
it.filled_rectangle(152, y + 42, w, 5);
}
}
y += 50;
}
# 7. Solar: production now and today, house and grid, graph of the last 16 h
- id: page_solar
lambda: |-
it.print(8, 8, id(icons_m), TextAlign::TOP_LEFT, "\U000F0A72");
const float sp = std::max(0.0f, id(solar_power).state);
it.print(50, 0, id(f_big), TextAlign::TOP_LEFT, el512::watts(sp).c_str());
if (!std::isnan(id(solar_today).state))
it.printf(50, 54, id(f_s), TextAlign::TOP_LEFT, "today %.2f kWh", id(solar_today).state / 1000.0f);
it.print(300, 8, id(icons_m), TextAlign::TOP_LEFT, "\U000F06A1");
it.print(340, 12, id(f_m), TextAlign::TOP_LEFT, el512::watts(id(house_power).state).c_str());
const float net = id(grid_import).state - id(grid_export).state;
it.print(300, 48, id(icons_m), TextAlign::TOP_LEFT, "\U000F0D3E");
it.print(340, 52, id(f_m), TextAlign::TOP_LEFT, el512::watts(std::fabs(net)).c_str());
it.print(440, 48, id(icons_m), TextAlign::TOP_LEFT, net > 0 ? "\U000F072E" : "\U000F0737");
it.graph(8, 128, id(solar_graph));
it.print(12, 110, id(f_s), TextAlign::TOP_LEFT, "last 16 h");
# 8. Portal end credits (started from HA; returns to the previous page)
- id: page_still_alive
lambda: |-
id(panel).draw_still_alive();
# 9-12. Cyberpunk-style animated pages (components/el512/cyberpunk.cpp)
- id: page_cyber_hud
lambda: |-
el512::cyber::netrunner_hud(*id(panel), id(cp_fonts), id(cp_home), id(ha_time).now());
- id: page_cyber_breach
lambda: |-
el512::cyber::breach_protocol(*id(panel), id(cp_fonts));
- id: page_cyber_scan
lambda: |-
el512::cyber::optics_scan(*id(panel), id(cp_fonts), id(cp_home), id(ha_time).now());
- id: page_cyber_split
lambda: |-
el512::cyber::split_hud(*id(panel), id(cp_fonts), id(cp_home), id(ha_time).now());
# 13-16. KonoSuba pages (components/el512/konosuba.cpp)
- id: page_ks_explosion
lambda: |-
el512::konosuba::explosion(*id(panel), id(ks_fonts), id(cp_home), id(ha_time).now());
- id: page_ks_guild
lambda: |-
el512::konosuba::guild_board(*id(panel), id(ks_fonts), id(cp_home), id(ha_time).now());
- id: page_ks_card
lambda: |-
el512::konosuba::adventurer_card(*id(panel), id(ks_fonts), id(cp_home), id(ha_time).now());
- id: page_ks_battle
lambda: |-
el512::konosuba::party_battle(*id(panel), id(ks_fonts), id(cp_home));
# 17-20. Steins;Gate pages (components/el512/steins.cpp)
- id: page_sg_meter
lambda: |-
el512::steins::divergence_meter(*id(panel), id(sg_fonts), id(ha_time).now());
- id: page_sg_dmail
lambda: |-
el512::steins::d_mail(*id(panel), id(sg_fonts), id(cp_home));
- id: page_sg_lab
lambda: |-
el512::steins::gadget_lab(*id(panel), id(sg_fonts), id(cp_home));
- id: page_sg_world
lambda: |-
el512::steins::world_lines(*id(panel), id(sg_fonts), id(cp_home), id(ha_time).now());
# 21-24. Star Trek / Lower Decks pages (components/el512/trek.cpp)
- id: page_st_lcars
lambda: |-
el512::trek::lcars_status(*id(panel), id(st_fonts), id(cp_home), id(ha_time).now());
- id: page_st_roster
lambda: |-
el512::trek::duty_roster(*id(panel), id(st_fonts), id(cp_home), id(ha_time).now());
- id: page_st_warp
lambda: |-
el512::trek::warp_core(*id(panel), id(st_fonts), id(cp_home));
- id: page_st_log
lambda: |-
el512::trek::captains_log(*id(panel), id(st_fonts), id(cp_home), id(ha_time).now());
# 25-28. Interstellar pages (components/el512/interstellar.cpp)
- id: page_is_gargantua
lambda: |-
el512::interstellar::gargantua(*id(panel), id(is_fonts), id(ha_time).now());
- id: page_is_tars
lambda: |-
el512::interstellar::tars(*id(panel), id(is_fonts), id(cp_home));
- id: page_is_endurance
lambda: |-
el512::interstellar::endurance(*id(panel), id(is_fonts), id(cp_home), id(ha_time).now());
- id: page_is_tesseract
lambda: |-
el512::interstellar::tesseract(*id(panel), id(is_fonts), id(cp_home), id(ha_time).now());
# 29-30. Your Name pages (components/el512/yourname.cpp)
- id: page_yn_comet
lambda: |-
el512::yourname::comet(*id(panel), id(yn_fonts), id(cp_home), id(ha_time).now());
- id: page_yn_twilight
lambda: |-
el512::yourname::twilight(*id(panel), id(yn_fonts), id(cp_home), id(ha_time).now());