Public Access
Driver (components/el512/el512_panel.*, ESP-IDF only): - DMA ring of 8 x 4 lines (17 KB) refilled from the I2S EOF interrupt (IRAM) instead of a 142 KB full-frame stream; ~107 KB heap freed. - 256-entry lookup table encodes 8 pixels per step. - Back buffer + displayed buffer, swapped at the start of a frame. - PlatformIO builds it via lib_extra_dirs (EL512Core); lib/EL512 keeps the Adafruit GFX front end. ESPHome external component (platform: el512): - DisplayBuffer with optional ENABLE / _LOWPWR pins, set_enabled(), set_low_power(), helper functions for page lambdas. - still_alive_page: plays the Portal end-credits show, then returns to the previous page. Still Alive engine moved to components/el512/still_alive.* with a built-in 5x7 font (from Adafruit GFX, BSD); the PlatformIO demo uses the same code. The importer writes components/el512/still_alive_data.h and stops at 200 s (--until-ms), matching the hand-trimmed data. esphome/el512-panel.yaml: 8 pages (clock, forecast, agenda, dashboard, now playing, Plex, solar, Still Alive), page select, rotation, display and low-power switches, message text. home_assistant/packages/el512_panel.yaml: helper template sensors for forecast, agenda, now playing and Plex streams. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
558 lines
20 KiB
YAML
558 lines
20 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}
|
|
|
|
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"
|
|
|
|
# --- 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
|
|
|
|
# --- 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
|
|
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);
|
|
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; skip Still Alive (start it from HA) and Now playing 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;
|
|
for (int i = 0; i < 8; i++) {
|
|
id(panel).show_next_page();
|
|
const auto *p = id(panel).get_active_page();
|
|
const std::string &np = id(np_state).state;
|
|
if (p == id(page_still_alive)) continue;
|
|
if (p == id(page_media) && (np == "idle" || np.empty())) continue;
|
|
break;
|
|
}
|
|
id(panel).update();
|
|
|
|
# --- 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
|
|
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";
|
|
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;
|
|
}
|
|
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();
|