# 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 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)}; 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" # Themed pages (Cyberpunk, KonoSuba, Steins;Gate): 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: 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 # 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 glyphs: " 0123456789ABCDEF.%" # 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: 110 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: "未来ガジェット研究所世界線" # --- 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 - 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 # 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 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); 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 is left out # (start it from HA) and Now playing is skipped when idle. interval: - interval: 1s then: - lambda: |- 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 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)}; 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.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] 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"; 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(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; } int y = 50; for (auto &s : streams) { if (y > 256 - 48) break; it.print(152, y, id(f_s), TextAlign::TOP_LEFT, el512::fit(id(f_s), el512::field(s, 0), 352).c_str()); it.print(152, y + 18, id(f_m), TextAlign::TOP_LEFT, el512::fit(id(f_m), el512::field(s, 1), 352).c_str()); 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());