I do not know how to upload yaml files, so here comes the code:
# substitution fort ESPHome and recalculate in lambda for drawing
# rectangle (top_x, top_y, x_width, y_width) in the drawing
substitutions:
# Button Bottom Right
button_br_x1: "680"
button_br_y1: "425"
button_br_x2: "890"
button_br_y2: "500"
# Button Bottom Left
button_bl_x1: "420" # 680 - length - padding = 680 - (button_br_x2 + button_br_x1) - 50
button_bl_y1: "${button_br_y1}" # same height as br but used for clarity below
button_bl_x2: "630" # button_bl_x1 - (button_br_x2 + button_br_x1)
button_bl_y2: "${button_br_y2}" # same height as br
# Button Bottom sleep
button_bs_x1: "60" #
button_bs_y1: "${button_br_y1}" # same height as br but used for clarity below
button_bs_x2: "270" #
button_bs_y2: "${button_br_y2}" # same height as br
# MDI fonts
# https://pictogrammers.com/library/mdi/
# WIFI
mdi_wifi: "\U000F05A9" # mdi-wifi
mdi_wifi_strength_4: "\U000F0928" # mdi-wifi-strength-4
mdi_wifi_strength_3: "\U000F0925" # mdi-wifi-strength-3
mdi_wifi_strength_2: "\U000F0922" # mdi-wifi-strength-2
mdi_wifi_strength_1: "\U000F091F" # mdi-wifi-strength-1
mdi_wifi_strength_alert_outline: "\U000F092B" # mdi-wifi-strength-alert-outline
mdi_access_point: "\U000F0003" # mdi-access-point
# battery
mdi_battery_100: "\U000F0079" # mdi-battery
mdi_battery_80: "\U000F0081" # mdi-battery-80
mdi_battery_60: "\U000F007F" # mdi-battery-60
mdi_battery_40: "\U000F007D" # mdi-battery-40
mdi_battery_20: "\U000F007B" # mdi-battery-20
mdi_battery_10: "\U000F007A" # mdi-battery-10
mdi_battery_charging_100: "\U000F0085" # mdi-battery-charging-100
mdi_battery_charging_80: "\U000F008A" # mdi-battery-charging-80
mdi_battery_charging_60: "\U000F0089" # mdi-battery-charging-60
mdi_battery_charging_40: "\U000F0088" # mdi-battery-charging-40
mdi_battery_charging_20: "\U000F0086" # mdi-battery-charging-20
mdi_battery_charging_10: "\U000F089C" # mdi-battery-charging-10
mdi_battery_unknown: "\U000F0091" # mdi-battery-unknown
# Misc
mdi_book_outline: "\U000F0B64" # mdi-book-outline
esphome:
name: "${rmgr_device_name}"
friendly_name: "${rmgr_device_name}"
platformio_options:
board_build.f_flash: 80000000L
board_build.flash_mode: qio
board_build.psram_type: opi
board_build.partitions: default_16MB.csv
board_build.arduino.memory_type: qio_opi
build_flags: # the first three defines are required for the screen library to function.
- "-DBOARD_HAS_PSRAM" #OK
- "-DARDUINO_USB_MODE=1" #OK
- "-DARDUINO_USB_CDC_ON_BOOT=1" #OK
libraries:
- SPI
on_boot:
- priority: -100
then:
- component.update: wifisignal
- component.update: battery_voltage
- priority: -800 # run as early as possible (must be <1000)
then:
- component.update: t5_display # show "Booting..." immediately
esp32:
board: esp32-s3-devkitc-1
framework:
type: arduino
variant: esp32s3
flash_size: 16MB
i2c:
- id: bus_a
sda: GPIO18
scl: GPIO17
frequency: 100khz
# There is some problems with i2c scan so turn scan off if problem appear on your board
scan: False
touchscreen:
platform: gt911
id: lilygo_touchscreen
interrupt_pin: GPIO47
address: 0x5D
i2c_id: bus_a
setup_priority: -100
transform:
# because we use the display widescreen, buttons on top we have to transfer the touch
mirror_x: false
mirror_y: true
swap_xy: true
on_update:
- lambda: |-
for (auto touch: touches) {
if (touch.state <= 2) {
ESP_LOGI("Touch points:", "id=%d x=%d, y=%d", touch.id, touch.x, touch.y);
}
}
# Have Buttons in HomeAssistant for actions
# defined above and substitutions used also in lambda for drawing the rectangle on screen !!!
binary_sensor:
- platform: status
name: "API Status"
id: api_status
- platform: touchscreen
name: "bottom left touch"
id: bl_touch
x_min: ${button_bl_x1}
y_min: ${button_bl_y1}
x_max: ${button_bl_x2}
y_max: ${button_bl_y2}
- platform: touchscreen
name: "bottom right touch"
id: br_touch
x_min: ${button_br_x1}
y_min: ${button_br_y1}
x_max: ${button_br_x2}
y_max: ${button_br_y2}
- platform: touchscreen
name: "bottom sleep touch"
id: bs_touch
x_min: ${button_bs_x1}
y_min: ${button_bs_y1}
x_max: ${button_bs_x2}
y_max: ${button_bs_y2}
# - platform: gpio
# pin:
# number: GPIO21 # SENS OP_VN
# inverted: true
# allow_other_uses: true
# name: "Button 1"
# on_press:
# - logger.log: "GPIO21 pressed"
text_sensor:
- platform: homeassistant
name: "Charging Status"
id: ha_charging_status # internal here in ESPHome YAML
entity_id: sensor.${rmgr_device_name}_battery_charging_status # ID in HA
globals:
- id: sleep_duration
type: int
restore_value: no
initial_value: '10' # default sleep duration in seconds
- id: overlay_text
type: std::string
restore_value: no
initial_value: ""
- id: is_booting
type: bool
restore_value: no
initial_value: 'true'
- id: is_going_to_sleep
type: bool
restore_value: no
initial_value: 'false'
# Not used in lilygo: but kept for compatibility to waveshare different display use (so we can use the same Perl)
- id: initial_data_received
type: bool
restore_value: no
initial_value: 'false'
- id: rmgr_booking_slot_length_minutes
type: std::string
restore_value: no
- id: last_display_refresh
type: std::string
restore_value: false
# used for both waveshare and lilygo
# Display globals:
- id: headline_text
type: std::string
restore_value: false
initial_value: ""
- id: subheadline_text
type: std::string
restore_value: false
initial_value: ""
# ready made times and summary from calendar, e.g. 10:05 - 13:15 whatever
- id: esp_drawing_events
type: std::vector<std::string>
restore_value: false
initial_value: 'std::vector<std::string>()'
# Enable logging
logger:
# level: NONE
# level: ERROR
# level: WARN
# level: INFO
level: DEBUG
# level: VERBOSE
# level: VERY_VERBOSE
# we need time :-)
time:
- platform: homeassistant
id: ha_time
# Enable Home Assistant API
api:
encryption:
key: !secret api_encryption_key
services:
- service: set_sleep_duration
variables:
duration: int
then:
- lambda: |-
id(sleep_duration) = duration;
ESP_LOGI("Sleep", "Sleep duration set to %d seconds", duration);
# MUSS Angepasst werden an das LilyGo!
- service: "draw_schedule"
variables:
param_headline_text: string
param_subheadline_text: string
param_rmgr_booking_slot_length_minutes: string
param_last_display_refresh: string
param_esp_drawing_events: string[]
then:
- lambda: 'id(initial_data_received) = true;'
- lambda: 'id(is_booting) = false;'
- lambda: 'id(is_going_to_sleep) = false;'
- lambda: 'id(overlay_text) = "";'
- lambda: 'id(headline_text) = param_headline_text;'
# - lambda: 'id(subheadline_text) = param_subheadline_text;'
- lambda: |-
id(subheadline_text) = param_subheadline_text;
ESP_LOGI("INFO", "subheadline_text: [%s]", id(subheadline_text).c_str());
# - lambda: 'id(esp_drawing_events) = param_esp_drawing_events;'
- lambda: |-
id(esp_drawing_events) = param_esp_drawing_events;
std::string joined;
for (const auto &item : param_esp_drawing_events) {
joined += item + ", ";
}
ESP_LOGI("INFO","esp_drawing_events: [%s]", joined.c_str());
- lambda: 'id(rmgr_booking_slot_length_minutes) = param_rmgr_booking_slot_length_minutes;'
- lambda: 'id(last_display_refresh) = param_last_display_refresh;'
- logger.log: ".-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-. calfreebusy_draw_schedule Data"
- logger.log:
format: "param_headline_text has value >%s<"
args: [ 'id(headline_text).c_str()' ]
- logger.log:
format: "param_subheadline_text has value >%s<"
args: [ 'id(subheadline_text).c_str()' ]
- component.update: t5_display
on_client_connected:
then:
- logger.log: "Connected to HomeAssistant. -> booted"
- homeassistant.event:
event: esphome.rmgr_device_booted
data:
device_name: "${rmgr_device_name}"
boot_time: !lambda |-
return id(ha_time).now().strftime("%Y-%m-%d %H:%M:%S");
- lambda: 'id(is_booting) = false;'
- lambda: 'id(is_going_to_sleep) = false;'
# - logger.log: "simulate refresh to refresh the screen on first connect"
# - homeassistant.event:
# event: esphome.${rmgr_device_name}
switch:
- platform: template
name: "Trigger Sleep"
id: ${rmgr_device_name}_trigger_sleep
restore_mode: ALWAYS_OFF
turn_on_action:
- script.execute: go_to_sleep
# scripts for putting ESP into sleep and informing the User on ePaper
script:
# to print on screen until when we sleep
- id: go_to_sleep
mode: restart
then:
- lambda: |-
auto now = id(ha_time).now();
auto timestamp = now.timestamp - 10; // seems to have 10 sec offset
auto nowtime = esphome::ESPTime::from_epoch_local(timestamp);
timestamp += id(sleep_duration);
// timestamp = now.timestamp + id(sleep_duration) - 10; seems to have 10 sec offset
auto future = esphome::ESPTime::from_epoch_local(timestamp);
// Map day_of_week (0 = saturday) to 3-letter name
const char* days[] = {"SAT", "SUN", "MON", "TUE", "WED", "THU", "FRI" };
int dow_future = future.day_of_week % 7; // ensure it's in 0..6
int dow = nowtime.day_of_week % 7; // ensure it's in 0..6
const char* day_str = "today";
char wake_time[50];
snprintf(wake_time, sizeof(wake_time), "Sleeping until: [%04d-%02d-%02d: %s] %02d:%02d:%02d",
future.year, future.month, future.day_of_month,
day_str,
future.hour, future.minute, future.second);
if (dow_future != dow) { // sleeping till another day, show different text
const char* day_str = days[dow_future];
snprintf(wake_time, sizeof(wake_time), "Sleeping until: [%s] %04d-%02d-%02d %02d:%02d:%02d",
day_str,
future.year, future.month, future.day_of_month,
future.hour, future.minute, future.second);
}
id(overlay_text) = wake_time;
id(is_going_to_sleep) = true;
ESP_LOGI("sleep", "Device will sleep until: %s", wake_time);
- component.update: t5_display
# - delay: 3s
- script.execute: sleep_now
# to immediately put ESP into sleep
- id: sleep_now
then:
- lambda: |-
ESP.deepSleep(id(sleep_duration) * 1000000ULL);
deep_sleep:
id: deep_sleep_wakeup_pin
esp32_ext1_wakeup:
pins: GPIO21
mode: ALL_LOW
# allow_other_uses: true
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Not needed, as the API connect takes way longer than the WIFI connect
#
# fast_connect: true
# manual_ip:
# static_ip: 172.16.0.200
# gateway: 172.16.0.1
# subnet: 255.255.255.0
external_components:
# https://github.com/nickolay/esphome-lilygo-t547plus
- source: github://nickolay/esphome-lilygo-t547plus
components: ["t547"]
# https://github.com/kaeltis/esphome-lilygo-t547plus
- source: github://kaeltis/esphome-lilygo-t547plus
components: ["lilygo_t5_47_battery"]
font:
- file: 'fonts/GothamRnd-Book.ttf'
id: font_medium_gotham
size: 30
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font_mdi_medlarge
size: 35
glyphs:
- "${mdi_wifi}"
- "${mdi_wifi_strength_4}"
- "${mdi_wifi_strength_3}"
- "${mdi_wifi_strength_2}"
- "${mdi_wifi_strength_1}"
- "${mdi_wifi_strength_alert_outline}"
- "${mdi_book_outline}"
# battery
- "${mdi_battery_100}"
- "${mdi_battery_80}"
- "${mdi_battery_60}"
- "${mdi_battery_40}"
- "${mdi_battery_20}"
- "${mdi_battery_10}"
- "${mdi_battery_charging_100}"
- "${mdi_battery_charging_80}"
- "${mdi_battery_charging_60}"
- "${mdi_battery_charging_40}"
- "${mdi_battery_charging_20}"
- "${mdi_battery_charging_10}"
- "${mdi_battery_unknown}"
display:
- platform: t547
id: t5_display
update_interval: never
lambda: |-
auto draw_rect_frame = [&](display::Display &it,
int x1, int y1, int x2, int y2,
int thickness,
std::string label = "",
esphome::font::Font* font = nullptr) {
int width = x2 - x1;
int height = y2 - y1;
// Draw borders
it.filled_rectangle(x1, y1, width, thickness); // top
it.filled_rectangle(x1, y2 - thickness, width, thickness); // bottom
it.filled_rectangle(x1, y1, thickness, height); // left
it.filled_rectangle(x2 - thickness, y1, thickness, height); // right
// Optional: draw label text centered inside
if (!label.empty() && font != nullptr) {
int cx = x1 + width / 2;
int cy = y1 + height / 2 + 5;
it.printf(cx, cy, font, TextAlign::CENTER, "%s", label.c_str());
}
};
auto draw_always = [&](display::Display &it) {
int max_x = it.get_width();
int max_y = it.get_height();
int event_x = 50;
int event_y = 50;
int line_height = 30; // Adjust based on your font size
size_t max_line_length = 50;
bool was_cut = false;
int bottom_margin = 250; // Keep this space clear
int max_event_y = max_y - bottom_margin;
for (const auto &event : id(esp_drawing_events)) {
size_t start = 0;
bool first_line = true;
while (start < event.length()) {
size_t remaining = event.length() - start;
size_t len = (remaining > max_line_length) ? max_line_length : remaining;
size_t end = start + len;
// Look backwards for last space before end
size_t space_pos = event.rfind(' ', end);
if (space_pos == std::string::npos || space_pos < start) {
space_pos = end; // no space found, force cut
}
std::string line = event.substr(start, space_pos - start);
int indent = first_line ? 0 : 30;
it.printf(event_x + indent, event_y, id(font_medium_gotham), TextAlign::TOP_LEFT, "%s", line.c_str());
event_y += line_height;
start = (space_pos < event.length()) ? space_pos + 1 : event.length();
first_line = false;
if (event_y > max_event_y) {
was_cut = true;
break;
}
}
// Add more spacing between different events
event_y += 15;
if (event_y > max_event_y) {
was_cut = true;
break;
}
}
// If content was cut, show a warning at the bottom
if (was_cut) {
it.printf(event_x + 50, event_y + line_height, id(font_medium_gotham), TextAlign::TOP_LEFT, "Text above !!! CUT !!!");
}
};
auto draw_wifi_icon = [&](display::Display &it) {
std::string wifi_symbol = "${mdi_access_point}"; // default if no state!
if (id(wifisignal).has_state()) {
float rssi = id(wifisignal).state;
if (rssi >= -50) {
wifi_symbol = "${mdi_wifi_strength_4}";
} else if (rssi >= -60) {
wifi_symbol = "${mdi_wifi_strength_3}";
} else if (rssi >= -67) {
wifi_symbol = "${mdi_wifi_strength_2}";
} else if (rssi >= -70) {
wifi_symbol = "${mdi_wifi_strength_1}";
} else {
wifi_symbol = "${mdi_wifi_strength_alert_outline}";
}
}
// soll neben die Buttons gedruckt werden ĂĽber wifi
it.printf(${button_br_x2} + 15, ${button_br_y1} + (${button_br_y2} - ${button_br_y1}) / 2 , id(font_mdi_medlarge), TextAlign::TOP_LEFT, wifi_symbol.c_str());
};
auto draw_battery_icon = [&](display::Display &it) {
std::string batt_symbol = "${mdi_battery_unknown}"; // default if no state!
if (id(battery_voltage_voltage).has_state()) {
float batt = id(battery_voltage_voltage).state;
bool charging = id(ha_charging_status).has_state() && id(ha_charging_status).state == "charging";
if (charging) {
if (batt >= 4.42) {
batt_symbol = "${mdi_battery_charging_100}";
} else if (batt >= 4.2) {
batt_symbol = "${mdi_battery_charging_80}";
} else if (batt >= 4) {
batt_symbol = "${mdi_battery_charging_60}";
} else if (batt >= 3.9) {
batt_symbol = "${mdi_battery_charging_40}";
} else if (batt >= 3.8) {
batt_symbol = "${mdi_battery_charging_20}";
} else {
batt_symbol = "${mdi_battery_charging_10}";
}
} else {
if (batt >= 4.35) {
batt_symbol = "${mdi_battery_100}";
} else if (batt >= 4.2) {
batt_symbol = "${mdi_battery_80}";
} else if (batt >= 4) {
batt_symbol = "${mdi_battery_60}";
} else if (batt >= 3.9) {
batt_symbol = "${mdi_battery_40}";
} else if (batt >= 3.8) {
batt_symbol = "${mdi_battery_20}";
} else {
batt_symbol = "${mdi_battery_10}";
}
}
}
// soll neben die Buttons gedruckt werden ĂĽber wifi
it.printf(${button_br_x2} + 15, ${button_br_y1} + (${button_br_y2} - ${button_br_y1}) / 2 - 40, id(font_mdi_medlarge), TextAlign::TOP_LEFT, batt_symbol.c_str());
};
auto draw_sleep_overlay = [&](display::Display &it) {
it.printf(${button_br_x2}, ${button_br_y1} + (${button_br_y2} - ${button_br_y1}) / 2,
id(font_medium_gotham), TextAlign::CENTER_RIGHT,
id(overlay_text).c_str());
};
auto draw_buttons = [&](display::Display &it) {
// below draw empty rectangle frame
// draw_rect_frame(it, ${button_bl_x1}, ${button_bl_y1}, ${button_bl_x2}, ${button_bl_y2}, 8);
// below draw rectanle with text centered
draw_rect_frame(it, ${button_bl_x1}, ${button_bl_y1}, ${button_bl_x2}, ${button_bl_y2}, 12, "15 Min", id(font_medium_gotham));
draw_rect_frame(it, ${button_br_x1}, ${button_br_y1}, ${button_br_x2}, ${button_br_y2}, 12, "30 Min", id(font_medium_gotham));
draw_rect_frame(it, ${button_bs_x1}, ${button_bs_y1}, ${button_bs_x2}, ${button_bs_y2}, 12, "SLEEP", id(font_medium_gotham));
//it.filled_rectangle(${button_bl_x1}, ${button_bl_y1},
// ${button_bl_x2} - ${button_bl_x1}, ${button_bl_y2} - ${button_bl_y1});
};
// ##### below the drawing happens
// What is printer before the "if" the stuff (after first else) is always on screen
if (id(is_booting)) {
it.printf(it.get_width() / 2, it.get_height() / 2, id(font_medium_gotham), TextAlign::CENTER, "Booting...");
id(is_booting) = false;
return;
} else {
// always on screen
draw_always(it);
draw_battery_icon(it);
// Differen print if Sleep Mode or not (Basically touch Buttons off in Sleep)
if (id(is_going_to_sleep) == true) {
// Here we go to Sleep Mode
draw_sleep_overlay(it);
} else {
// Printed in NON Sleep Mode
draw_buttons(it);
draw_wifi_icon(it);
}
}
sensor:
- platform: lilygo_t5_47_battery
id: battery_voltage
update_interval: 600s # we sleep a lot, and call it during wakeup
voltage:
name: "Battery Voltage"
id: battery_voltage_voltage
- platform: wifi_signal
name: "WiFi Signal Strength"
id: "wifisignal"
unit_of_measurement: "dBm"
entity_category: "diagnostic"
# update not really needed due to deep_sleep
update_interval: never