Public Access
The rotation already waits while the show plays, and the show returns to the page it came from when it ends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
800 lines
30 KiB
YAML
800 lines
30 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
|
|
|
|
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"
|
|
- id: plex_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
|
|
- "\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
|
|
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
|
|
on_value:
|
|
- lambda: id(plex_updated) = millis();
|
|
- 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 included:
|
|
# rotation waits while it plays); 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<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_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.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<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());
|