|
|
|
# WEATHERMAN DASHBOARD
|
|
|
|
# For Home Assistant and ESPHome
|
|
|
|
# Designed by Madelena Mak 2022 - https://mmak.es
|
|
|
|
|
|
|
|
# Cue "Blame it on the Weatherman" by B*Witched!
|
|
|
|
esphome:
|
|
|
|
name: "weatherman"
|
|
|
|
on_boot:
|
|
|
|
priority: 200.0
|
|
|
|
then:
|
|
|
|
- component.update: eink_display
|
|
|
|
|
|
|
|
esp32:
|
|
|
|
board: esp32dev
|
|
|
|
framework:
|
|
|
|
type: arduino
|
|
|
|
|
|
|
|
# Enable logging
|
|
|
|
logger:
|
|
|
|
|
|
|
|
# Enable Home Assistant API
|
|
|
|
api:
|
|
|
|
|
|
|
|
ota:
|
|
|
|
|
|
|
|
globals:
|
|
|
|
- id: data_updated
|
|
|
|
type: bool
|
|
|
|
restore_value: no
|
|
|
|
initial_value: 'false'
|
|
|
|
- id: initial_data_received
|
|
|
|
type: bool
|
|
|
|
restore_value: no
|
|
|
|
initial_value: 'false'
|
|
|
|
|
|
|
|
time:
|
|
|
|
- platform: homeassistant
|
|
|
|
id: homeassistant_time
|
|
|
|
on_time:
|
|
|
|
- seconds: 30
|
|
|
|
then:
|
|
|
|
- if:
|
|
|
|
condition:
|
|
|
|
lambda: 'return id(data_updated) == true;'
|
|
|
|
then:
|
|
|
|
- lambda: 'id(initial_data_received) = true;'
|
|
|
|
- logger.log: "Sensor data updated: Refreshing display..."
|
|
|
|
- component.update: eink_display
|
|
|
|
- lambda: 'id(data_updated) = false;'
|
|
|
|
else:
|
|
|
|
- logger.log: "No sensors updated - skipping display refresh."
|
|
|
|
|
|
|
|
wifi:
|
|
|
|
ssid: !secret wifi_ssid
|
|
|
|
password: !secret wifi_password
|
|
|
|
|
|
|
|
# Enable fallback hotspot (captive portal) in case wifi connection fails
|
|
|
|
ap:
|
|
|
|
ssid: "Esphome-Web-901078"
|
|
|
|
password: "2JOrpBYEHQsV"
|
|
|
|
|
|
|
|
|
|
|
|
# Include custom fonts
|
|
|
|
font:
|
|
|
|
- file: 'fonts/GothamRnd-Book.ttf'
|
|
|
|
id: font_small_book
|
|
|
|
size: 18
|
|
|
|
- file: 'fonts/GothamRnd-Bold.ttf'
|
|
|
|
id: font_large_bold
|
|
|
|
size: 108
|
|
|
|
glyphs: [' ', '°', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'C']
|
|
|
|
- file: 'fonts/GothamRnd-Bold.ttf'
|
|
|
|
id: font_title
|
|
|
|
size: 54
|
|
|
|
glyphs: ['W', 'E', 'A', 'T', 'H', 'R', 'L', 'I', 'N', ' ']
|
|
|
|
- file: 'fonts/GothamRnd-Bold.ttf'
|
|
|
|
id: font_medium_bold
|
|
|
|
size: 30
|
|
|
|
# glyphs: [' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'M', 'I', 'N']
|
|
|
|
- file: 'fonts/GothamRnd-Bold.ttf'
|
|
|
|
id: font_small_bold
|
|
|
|
size: 18
|
|
|
|
# glyphs: ['°', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'C', 'M', 'I', 'N']
|
|
|
|
|
|
|
|
|
|
|
|
# Include Material Design Icons font
|
|
|
|
# Thanks to https://community.home-assistant.io/t/display-materialdesign-icons-on-esphome-attached-to-screen/199790/16
|
|
|
|
- file: 'fonts/materialdesignicons-webfont.ttf'
|
|
|
|
id: font_mdi_large
|
|
|
|
size: 96
|
|
|
|
glyphs: &mdi-weather-glyphs
|
|
|
|
- "\U000F0590" # mdi-weather-cloudy
|
|
|
|
- "\U000F0F2F" # mdi-weather-cloudy-alert
|
|
|
|
- "\U000F0E6E" # mdi-weather-cloudy-arrow-right
|
|
|
|
- "\U000F0591" # mdi-weather-fog
|
|
|
|
- "\U000F0592" # mdi-weather-hail
|
|
|
|
- "\U000F0F30" # mdi-weather-hazy
|
|
|
|
- "\U000F0898" # mdi-weather-hurricane
|
|
|
|
- "\U000F0593" # mdi-weather-lightning
|
|
|
|
- "\U000F067E" # mdi-weather-lightning-rainy
|
|
|
|
- "\U000F0594" # mdi-weather-night
|
|
|
|
- "\U000F0F31" # mdi-weather-night-partly-cloudy
|
|
|
|
- "\U000F0595" # mdi-weather-partly-cloudy
|
|
|
|
- "\U000F0F32" # mdi-weather-partly-lightning
|
|
|
|
- "\U000F0F33" # mdi-weather-partly-rainy
|
|
|
|
- "\U000F0F34" # mdi-weather-partly-snowy
|
|
|
|
- "\U000F0F35" # mdi-weather-partly-snowy-rainy
|
|
|
|
- "\U000F0596" # mdi-weather-pouring
|
|
|
|
- "\U000F0597" # mdi-weather-rainy
|
|
|
|
- "\U000F0598" # mdi-weather-snowy
|
|
|
|
- "\U000F0F36" # mdi-weather-snowy-heavy
|
|
|
|
- "\U000F067F" # mdi-weather-snowy-rainy
|
|
|
|
- "\U000F0599" # mdi-weather-sunny
|
|
|
|
- "\U000F0F37" # mdi-weather-sunny-alert
|
|
|
|
- "\U000F14E4" # mdi-weather-sunny-off
|
|
|
|
- "\U000F059A" # mdi-weather-sunset
|
|
|
|
- "\U000F059B" # mdi-weather-sunset-down
|
|
|
|
- "\U000F059C" # mdi-weather-sunset-up
|
|
|
|
- "\U000F0F38" # mdi-weather-tornado
|
|
|
|
- "\U000F059D" # mdi-weather-windy
|
|
|
|
- "\U000F059E" # mdi-weather-windy-variant
|
|
|
|
- file: 'fonts/materialdesignicons-webfont.ttf'
|
|
|
|
id: font_mdi_medium
|
|
|
|
size: 36
|
|
|
|
glyphs: *mdi-weather-glyphs
|
|
|
|
|
|
|
|
|
|
|
|
# Include Custom Titles
|
|
|
|
image:
|
|
|
|
- file: "images/weatherman-title-train.png"
|
|
|
|
id: title_train
|
|
|
|
type: BINARY
|
|
|
|
- file: "images/weatherman-title-weather.png"
|
|
|
|
id: title_weather
|
|
|
|
type: BINARY
|
|
|
|
|
|
|
|
|
|
|
|
# Call Subway and Weather sensors from HA
|
|
|
|
sensor:
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.gtfs_mta_subway_manhattan
|
|
|
|
id: train_manhattan_due_in
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.gtfs_mta_subway_canarsie
|
|
|
|
id: train_canarsie_due_in
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.gtfs_mta_subway_manhattan
|
|
|
|
attribute: Next bus due in
|
|
|
|
id: train_manhattan_next_train_due_in
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.gtfs_mta_subway_canarsie
|
|
|
|
attribute: Next bus due in
|
|
|
|
id: train_canarsie_next_train_due_in
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: weather.hourly
|
|
|
|
attribute: temperature
|
|
|
|
id: weather_temperature
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_temperature_0
|
|
|
|
id: weather_temperature_0
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_temperature_1
|
|
|
|
id: weather_temperature_1
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_temperature_2
|
|
|
|
id: weather_temperature_2
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_temperature_3
|
|
|
|
id: weather_temperature_3
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: wifi_signal
|
|
|
|
name: "WiFi Signal Sensor"
|
|
|
|
id: wifisignal
|
|
|
|
update_interval: 60s
|
|
|
|
|
|
|
|
|
|
|
|
text_sensor:
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.gtfs_mta_subway_manhattan
|
|
|
|
attribute: Due at
|
|
|
|
id: train_manhattan_due_at
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.gtfs_mta_subway_canarsie
|
|
|
|
attribute: Due at
|
|
|
|
id: train_canarsie_due_at
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.gtfs_mta_subway_manhattan
|
|
|
|
attribute: Next bus
|
|
|
|
id: train_manhattan_next_train_due_at
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.gtfs_mta_subway_canarsie
|
|
|
|
attribute: Next bus
|
|
|
|
id: train_canarsie_next_train_due_at
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.mta_subway_l_service_status
|
|
|
|
id: train_service_status
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: weather.hourly
|
|
|
|
id: weather_state
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_condition_now
|
|
|
|
id: weather_condition_now
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_condition_0
|
|
|
|
id: weather_condition_0
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_timestamp_0
|
|
|
|
id: weather_timestamp_0
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_condition_1
|
|
|
|
id: weather_condition_1
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_timestamp_1
|
|
|
|
id: weather_timestamp_1
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_condition_2
|
|
|
|
id: weather_condition_2
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_timestamp_2
|
|
|
|
id: weather_timestamp_2
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_condition_3
|
|
|
|
id: weather_condition_3
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: weather_timestamp_3
|
|
|
|
id: weather_timestamp_3
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: train_status
|
|
|
|
id: train_status
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: train_status_manhattan
|
|
|
|
id: train_status_manhattan
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
- platform: homeassistant
|
|
|
|
entity_id: sensor.weatherman_data
|
|
|
|
attribute: train_status_canarsie
|
|
|
|
id: train_status_canarsie
|
|
|
|
on_value:
|
|
|
|
then:
|
|
|
|
- lambda: 'id(data_updated) = true;'
|
|
|
|
|
|
|
|
|
|
|
|
# Define colors
|
|
|
|
# This design is white on black so this is necessary.
|
|
|
|
color:
|
|
|
|
- id: color_black
|
|
|
|
red: 0%
|
|
|
|
green: 0%
|
|
|
|
blue: 0%
|
|
|
|
white: 50%
|
|
|
|
- id: color_white
|
|
|
|
red: 0%
|
|
|
|
green: 0%
|
|
|
|
blue: 0%
|
|
|
|
white: 0%
|
|
|
|
|
|
|
|
|
|
|
|
# Pins for Waveshare ePaper ESP Board
|
|
|
|
spi:
|
|
|
|
clk_pin: GPIO13
|
|
|
|
mosi_pin: GPIO14
|
|
|
|
|
|
|
|
|
|
|
|
# Now render everything on the ePaper screen.
|
|
|
|
display:
|
|
|
|
- platform: waveshare_epaper
|
|
|
|
cs_pin: GPIO15
|
|
|
|
dc_pin: GPIO27
|
|
|
|
busy_pin: GPIO25
|
|
|
|
reset_pin: GPIO26
|
|
|
|
model: 7.50inV2
|
|
|
|
update_interval: never
|
|
|
|
id: eink_display
|
|
|
|
rotation: 90°
|
|
|
|
lambda: |-
|
|
|
|
// Map weather states to MDI characters.
|
|
|
|
std::map<std::string, std::string> weather_icon_map
|
|
|
|
{
|
|
|
|
{"cloudy", "\U000F0590"},
|
|
|
|
{"cloudy-alert", "\U000F0F2F"},
|
|
|
|
{"cloudy-arrow-right", "\U000F0E6E"},
|
|
|
|
{"fog", "\U000F0591"},
|
|
|
|
{"hail", "\U000F0592"},
|
|
|
|
{"hazy", "\U000F0F30"},
|
|
|
|
{"hurricane", "\U000F0898"},
|
|
|
|
{"lightning", "\U000F0593"},
|
|
|
|
{"lightning-rainy", "\U000F067E"},
|
|
|
|
{"night", "\U000F0594"},
|
|
|
|
{"night-partly-cloudy", "\U000F0F31"},
|
|
|
|
{"partlycloudy", "\U000F0595"},
|
|
|
|
{"partly-lightning", "\U000F0F32"},
|
|
|
|
{"partly-rainy", "\U000F0F33"},
|
|
|
|
{"partly-snowy", "\U000F0F34"},
|
|
|
|
{"partly-snowy-rainy", "\U000F0F35"},
|
|
|
|
{"pouring", "\U000F0596"},
|
|
|
|
{"rainy", "\U000F0597"},
|
|
|
|
{"snowy", "\U000F0598"},
|
|
|
|
{"snowy-heavy", "\U000F0F36"},
|
|
|
|
{"snowy-rainy", "\U000F067F"},
|
|
|
|
{"sunny", "\U000F0599"},
|
|
|
|
{"sunny-alert", "\U000F0F37"},
|
|
|
|
{"sunny-off", "\U000F14E4"},
|
|
|
|
{"sunset", "\U000F059A"},
|
|
|
|
{"sunset-down", "\U000F059B"},
|
|
|
|
{"sunset-up", "\U000F059C"},
|
|
|
|
{"tornado", "\U000F0F38"},
|
|
|
|
{"windy", "\U000F059D"},
|
|
|
|
{"windy-variant", "\U000F059E"},
|
|
|
|
};
|
|
|
|
|
|
|
|
// Fill background in black.
|
|
|
|
it.fill(color_black);
|
|
|
|
|
|
|
|
if (id(initial_data_received) == false) {
|
|
|
|
// ATTENTION!
|
|
|
|
// Please adjust coordinates I have a different display!
|
|
|
|
// ATTENTION!
|
|
|
|
it.printf(0, 420, id(font_small_bold), color_white, TextAlign::TOP_CENTER, "Waiting for data...");
|
|
|
|
} else {
|
|
|
|
// Weather Section
|
|
|
|
// it.image(0, 88, id(title_weather));
|
|
|
|
it.printf(240, 84, id(font_title), color_text, TextAlign::TOP_CENTER, "WEATHER");
|
|
|
|
|
|
|
|
it.printf(100, 158, id(font_mdi_large), color_white, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_now).state.c_str()].c_str());
|
|
|
|
|
|
|
|
it.printf(300, 158, id(font_large_bold), color_white, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature).state);
|
|
|
|
|
|
|
|
it.printf(105, 282, id(font_small_book), color_white, TextAlign::TOP_CENTER, "%s", id(weather_timestamp_0).state.c_str());
|
|
|
|
it.printf(105, 306, id(font_mdi_medium), color_white, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_0).state.c_str()].c_str());
|
|
|
|
it.printf(105, 354, id(font_small_bold), color_white, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature_0).state);
|
|
|
|
|
|
|
|
it.printf(195, 282, id(font_small_book), color_white, TextAlign::TOP_CENTER, "%s", id(weather_timestamp_1).state.c_str());
|
|
|
|
it.printf(195, 306, id(font_mdi_medium), color_white, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_1).state.c_str()].c_str());
|
|
|
|
it.printf(195, 354, id(font_small_bold), color_white, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature_1).state);
|
|
|
|
|
|
|
|
it.printf(285, 282, id(font_small_book), color_white, TextAlign::TOP_CENTER, "%s", id(weather_timestamp_2).state.c_str());
|
|
|
|
it.printf(285, 306, id(font_mdi_medium), color_white, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_2).state.c_str()].c_str());
|
|
|
|
it.printf(285, 354, id(font_small_bold), color_white, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature_2).state);
|
|
|
|
|
|
|
|
it.printf(375, 282, id(font_small_book), color_white, TextAlign::TOP_CENTER, "%s", id(weather_timestamp_3).state.c_str());
|
|
|
|
it.printf(375, 306, id(font_mdi_medium), color_white, TextAlign::TOP_CENTER, "%s", weather_icon_map[id(weather_condition_3).state.c_str()].c_str());
|
|
|
|
it.printf(375, 354, id(font_small_bold), color_white, TextAlign::TOP_CENTER, "%2.0f°C", id(weather_temperature_3).state);
|
|
|
|
|
|
|
|
// Train Service Section
|
|
|
|
// it.image(0, 420, id(title_train));
|
|
|
|
it.printf(240, 408, id(font_title), color_text, TextAlign::TOP_CENTER, "L TRAIN");
|
|
|
|
|
|
|
|
it.print(150, 536, id(font_small_bold), color_white, TextAlign::TOP_CENTER, "TO MANHATTAN");
|
|
|
|
it.print(330, 536, id(font_small_bold), color_white, TextAlign::TOP_CENTER, "TO CANARSIE");
|
|
|
|
|
|
|
|
it.printf(150, 576, id(font_medium_bold), color_white, TextAlign::TOP_CENTER, "%2.0f MIN", id(train_manhattan_due_in).state);
|
|
|
|
it.printf(330, 576, id(font_medium_bold), color_white, TextAlign::TOP_CENTER, "%2.0f MIN", id(train_canarsie_due_in).state);
|
|
|
|
it.printf(150, 608, id(font_small_book), color_white, TextAlign::TOP_CENTER, "%s", id(train_manhattan_due_at).state.c_str());
|
|
|
|
it.printf(330, 608, id(font_small_book), color_white, TextAlign::TOP_CENTER, "%s", id(train_canarsie_due_at).state.c_str());
|
|
|
|
|
|
|
|
it.printf(150, 652, id(font_medium_bold), color_white, TextAlign::TOP_CENTER, "%2.0f MIN", id(train_manhattan_next_train_due_in).state);
|
|
|
|
it.printf(330, 652, id(font_medium_bold), color_white, TextAlign::TOP_CENTER, "%2.0f MIN", id(train_canarsie_next_train_due_in).state);
|
|
|
|
it.printf(150, 684, id(font_small_book), color_white, TextAlign::TOP_CENTER, "%s", id(train_manhattan_next_train_due_at).state.c_str());
|
|
|
|
it.printf(330, 684, id(font_small_book), color_white, TextAlign::TOP_CENTER, "%s", id(train_canarsie_next_train_due_at).state.c_str());
|
|
|
|
}
|
|
|
|
captive_portal:
|