# Inspired by https://github.com/Madelena/esphome-weatherman-dashboard # And https://blog.wijman.net/e-ink-weather-frame-with-esphome-and-home-assistant/ # With a lot of my own additions, fixes, edits. My github: https://github.com/trip5 substitutions: name: eink-tasksboard friendly_name: eInk-TasksBoard project_name: "Trip5.eInk-TasksBoard" comment: "eInk-TasksBoard from Trip5" project_version: "2026.02.22" time_zone: "Asia/Seoul" # Check https://github.com/trip5/timezones.json/blob/master/timezones.md adc_pin: GPIO34 sleep_time: 600min # this is the default but is overridden # Home Assistant entitites: ha_disable_update: input_boolean.eink_tasksboard_disable_update ha_disable_deep_sleep: input_boolean.eink_tasksboard_disable_deep_sleep ha_tasks_data: sensor.eink_tasksboard_data ha_wake_times: sensor.eink_tasksboard_wake_times # A physical switch to disable deep sleep (optional, if no physical switch is attached then no problem) deep_sleep_disable_pin: GPIO21 # Waveshare eInk Board: status_led_pin: GPIO2 mosi_pin: GPIO14 clk_pin: GPIO13 cs_pin: GPIO15 dc_pin: GPIO27 reset_pin: GPIO26 busy_pin: GPIO25 busy_inverted: "true" #needed on most newer boards: check ESPHome or component notes # Everything here is meant for an 7.5 inch tricolor display, which is (as of 2024.12.0) now directly supported by ESPHome (thanks, JonasB2497!) # See here for full list of directly-supported displays: https://esphome.io/components/display/waveshare_epaper.html # if using only BW, then you may need to edit colors below (change red to same as black) model: 7.50in-bv3-bwr esphome: name: ${name} comment: "${comment}" project: name: "${project_name}" version: "${project_version}" on_boot: priority: -100.0 then: # - lambda: 'id(recorded_display_refresh) = 2756;' # if you need to manually update the number of refreshes (flash once with a set value then comment the line and reflash again) - script.execute: led_blink - script.execute: update_diagnostic_sensors - wait_until: condition: api.connected - if: condition: or: - binary_sensor.is_on: deep_sleep_disable_ha # checks Home Assistant Helper - binary_sensor.is_on: deep_sleep_disable_switch # checks physical switch then: - script.execute: deep_sleep_evaluation else: - script.execute: get_data_then_refresh_screen esp32: board: esp32dev framework: type: arduino preferences: flash_write_interval: 5s # default is 1min but seems sometimes id(recorded_display_refresh) doesn't get saved unless this is short ota: - platform: esphome password: !secret ota_password wifi: networks: - ssid: !secret wifi_ssid password: !secret wifi_password fast_connect: true ap: ssid: ${name} password: !secret ap_password web_server: port: 80 # include_internal: true # probably not necessary, as it will expose more than needed to the Web UI captive_portal: # Enable logging (and stops the LED from spamming the log) - it would also be OK to change the main level to ERROR logger: level: DEBUG logs: light: ERROR script: ERROR sensor: ERROR # Enable Home Assistant API api: # delete next 2 lines if not using HA API encryption encryption: key: !secret encryption_key button: - platform: restart name: "${friendly_name} Restart" id: "Restart" internal: true - platform: template name: "${friendly_name} Refresh Screen" entity_category: config on_press: - lambda: 'id(data_updated) = true;' - script.execute: refresh_screen output: - platform: ledc id: statusled pin: number: ${status_led_pin} ignore_strapping_warning: true light: - platform: monochromatic id: led1 name: "LED" output: statusled internal: true # Define colors - this design is white on black so this is necessary color: - id: color_blk red: 100% green: 100% blue: 100% white: 100% - id: color_red red: 100% green: 0% blue: 0% white: 0% # Pins for Waveshare ePaper ESP Board spi: clk_pin: ${clk_pin} mosi_pin: ${mosi_pin} # Global variables for detecting if the display needs to be refreshed. globals: - id: data_updated type: bool restore_value: no initial_value: 'false' - id: wake_time_helper_updated type: bool restore_value: no initial_value: 'false' - id: recorded_display_refresh type: int restore_value: yes initial_value: '0' script: - id: get_data_then_refresh_screen mode: restart then: - wait_until: condition: lambda: 'return id(data_updated) == true;' timeout: 60s - wait_until: # should wait until the wake_time_helper is actually updated (to evaluate the next condition) condition: lambda: 'return id(wake_time_helper_updated) == true;' timeout: 2min - if: # with longer sleep delays, the ESP32 tends to wake up a bit too early... if the wake time is within the next 8 minutes, wait for the scheduled wake time (less 30s) (preventing it from updating twice in a row) condition: lambda: 'return (id(wake_time_helper).state <= (8 * 60));' then: # if our calculated delay for waking is less than 0, then set it to 0 (to prevent negative delay) (returned as value in millseconds) - delay: !lambda 'return (((id(wake_time_helper).state - 30) < 0 ? 0 : (id(wake_time_helper).state - 30)) * 1000);' - if: condition: lambda: 'return id(data_updated) == true;' then: - delay: 15s # Wait a bit longer so all the items are received. Might need to be more (5s not enough, 10s sometimes isn't enough) - logger.log: "Sensor data received." - script.execute: refresh_screen else: - logger.log: "No new data. Skipping display refresh." - script.execute: deep_sleep_evaluation - id: refresh_screen mode: restart then: - if: condition: binary_sensor.is_off: disable_update then: - lambda: 'id(recorded_display_refresh) += 1;' - lambda: 'id(display_last_update).publish_state(id(homeassistant_time).now().timestamp);' - logger.log: "Refreshing display..." - component.update: eink_display - lambda: 'id(data_updated) = false;' - script.execute: update_diagnostic_sensors - delay: 5s # If deep sleep follows immediately, then we need a short delay here so HA catches the refresh count else: - logger.log: "Home Assistant Helper 'Disable Update' is on. Skipping display refresh." - script.execute: deep_sleep_evaluation - id: deep_sleep_evaluation # This dynamic deep sleep script inspired by https://webbinaro.dev/blog/battery-powered-esp-sensors-esphome/ mode: restart then: - if: condition: or: - binary_sensor.is_on: deep_sleep_disable_ha # checks Home Assistant Helper - binary_sensor.is_on: deep_sleep_disable_switch # checks physical switch then: - logger.log: 'Deep Sleep Disabled.' - script.execute: deep_sleep_disabled_check_refresh_time else: - wait_until: condition: lambda: 'return (id(wake_time_helper).state >= (6 * 60));' - script.stop: led_blink - light.turn_off: led1 - deep_sleep.enter: id: deep_sleep_control sleep_duration: !lambda return id(wake_time_helper).state * 1000; # lambdas should return value as ms - delay: 60s - script.execute: deep_sleep_evaluation - id: deep_sleep_disabled_check_refresh_time # This will update the screen on the HA helper schedule(ish) even if Deep Sleep is disabled mode: restart then: - if: condition: - lambda: 'return id(wake_time_helper).state <= 60;' then: - logger.log: 'Time to refresh the screen.' - script.execute: get_data_then_refresh_screen - id: led_blink # This will make a breathing effect on the status LED if the power is on mode: restart then: - delay: 1s - light.turn_off: led1 - delay: 4s - light.turn_on: led1 - script.execute: led_blink - id: update_diagnostic_sensors # This will update the diagnostic sensors mode: restart then: - component.update: recorded_display_refreshes - component.update: display_last_update - component.update: wifisignal - component.update: battery_adc - delay: 60s - script.execute: update_diagnostic_sensors deep_sleep: id: deep_sleep_control sleep_duration: ${sleep_time} time: - platform: homeassistant timezone: "${time_zone}" id: homeassistant_time # if using USB battery, deep sleep is disabled, and more constant refreshes are OK, then you may need this bit: #on_time: # - seconds: 0 # minutes: 0,15,30,45 # then: # - script.execute: get_data_then_refresh_screen binary_sensor: - platform: template name: "${friendly_name} Battery Warning" id: battery_warning device_class: battery lambda: |- if (id(battery_adc).state <= 3.4) { return true; } else { return false; } - platform: gpio # This sensor is for physical switch to enable deep sleep pin: number: ${deep_sleep_disable_pin} inverted: true mode: input: true pullup: true name: "${friendly_name} Disable Deep Sleep Switch" id: deep_sleep_disable_switch # The following are boolean helpers in Home Assistant - platform: homeassistant # This sensor is for Home Assistant helper switch to disable deep sleep, remove if not used id: deep_sleep_disable_ha name: "${friendly_name} Disable Deep Sleep HA" entity_id: '${ha_disable_deep_sleep}' - platform: homeassistant # This sensor is for Home Assistant helper switch to disable updates id: disable_update name: "${friendly_name} Disable Update" entity_id: '${ha_disable_update}' # Include custom fonts: font: # Gotham Rounded - for headers and text (by Tobias Frere Jones) # Get the Book version here: https://fontsgeek.com/fonts/Gotham-Book # Get the Bold version here: https://fontsgeek.com/fonts/Gotham-Rounded-Bold # Get the Medium version here: https://fontsgeek.com/fonts/Gotham-Rounded-Medium # The glyphs must be present or you'll see boxes/blank spaces (no problem unless you edit the Display section below) - don't forget to include a space! # How to get an easy glyphs list? Copy the code from here: https://www.geeksforgeeks.org/cpp-program-to-remove-duplicates-from-a-given-string/ # ...to here: https://www.onlinegdb.com/online_c++_compiler - file: 'fonts/Gotham-Rounded-Bold.ttf' id: font_title size: 52 glyphs: |- T LOADING - file: 'fonts/Gotham-Rounded-Bold.ttf' id: font_small_title size: 30 glyphs: |- UPCOMING - file: 'fonts/Gotham-Rounded-Book.ttf' id: font_refresh_stamp size: 20 glyphs: |- Refrshd at1234567890:AMPSunoTWFi # The fonts below this point will be used to display tasks - if you see black squares on your tasksboard, you forgot to include it on the glyphs list! # Easy to forget characters include currency marks. The glyphs under font_today_list and font_upcoming_item should match the filter in your Home Assistant YAML! - file: 'fonts/Gotham-Rounded-Bold.ttf' id: font_today_list size: 26 glyphs: |- ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz° - file: 'fonts/Gotham-Rounded-Medium.ttf' id: font_upcoming_date size: 20 glyphs: |- ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz° - file: 'fonts/Gotham-Rounded-Bold.ttf' id: font_upcoming_item size: 22 glyphs: |- ! "#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz° # Include Material Design Icons font (for battery icons) (by Austin Andrews / Templarian) # Get it here: https://github.com/Templarian/MaterialDesign-Webfont/ # Encoding glyphs from here: https://community.home-assistant.io/t/display-materialdesign-icons-on-esphome-attached-to-screen/199790/16 - file: 'fonts/materialdesignicons-webfont.ttf' id: font_battery_glyphs size: 22 glyphs: &mdi-battery-glyphs - "\U000F10CD" # mdi-battery-warning - "\U000F12A3" # mdi-battery-high - "\U000F12A2" # mdi-battery-med - "\U000F12A1" # mdi-battery-low sensor: # Create sensors for monitoring the board remotely. - platform: template name: "${friendly_name} Display Last Update" id: display_last_update update_interval: never device_class: timestamp entity_category: "diagnostic" - platform: template name: "${friendly_name} Recorded Display Refresh" id: recorded_display_refreshes update_interval: never accuracy_decimals: 0 unit_of_measurement: "Refreshes" state_class: "total_increasing" entity_category: "diagnostic" lambda: 'return id(recorded_display_refresh);' - platform: wifi_signal # actually called from a script name: "${friendly_name} WiFi Signal Strength" id: wifisignal update_interval: never unit_of_measurement: "dBm" entity_category: "diagnostic" - platform: adc # get an unfiltered reading from the ADC pin name: "${friendly_name} Battery Unfiltered" id: battery_adc_raw update_interval: never pin: ${adc_pin} attenuation: 12db entity_category: "diagnostic" accuracy_decimals: 2 device_class: battery unit_of_measurement: "V" internal: true filters: # this multiply number may need to be manually adjusted # I used a 3.7V LiPo battery: 5V -> 100K resistor -> ADC -> 100K resistor -> GND # ADC -> 100nF capacitor -> GND - multiply: 2 - platform: template # This gets 5 readings (quickly) and calculates a median name: "${friendly_name} Battery" id: battery_adc update_interval: never entity_category: "diagnostic" accuracy_decimals: 2 device_class: battery unit_of_measurement: "V" lambda: |- float readings[5]; for (int i = 0; i < 5; i++) { id(battery_adc_raw).update(); delay(50); // wait for stable reading readings[i] = id(battery_adc_raw).state; } std::sort(readings, readings + 5); return readings[2]; // median value # Call numeric sensors from Home Assistant - platform: homeassistant name: "${friendly_name} Wake Time Helper" id: wake_time_helper entity_id: '${ha_wake_times}' attribute: wake_time_helper unit_of_measurement: seconds on_value: then: - lambda: 'id(wake_time_helper_updated) = true;' text_sensor: - platform: version name: "${friendly_name} ESPHome Version" hide_timestamp: true entity_category: diagnostic - platform: wifi_info ip_address: id: wifi_ip name: "${friendly_name} IP Address" entity_category: diagnostic ssid: id: wifi_ssid name: "${friendly_name} SSID" entity_category: diagnostic - platform: template name: "${friendly_name} ${project_name} Version" lambda: |- return {"${project_version}"}; entity_category: diagnostic # Call text sensors from Home Assistant - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_0 id: today_0 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_1 id: today_1 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_2 id: today_2 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_3 id: today_3 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_4 id: today_4 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_5 id: today_5 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_6 id: today_6 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_7 id: today_7 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_8 id: today_8 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: today_9 id: today_9 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_0 id: upcoming_date_0 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_0 id: upcoming_item_0 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_1 id: upcoming_date_1 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_1 id: upcoming_item_1 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_2 id: upcoming_date_2 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_2 id: upcoming_item_2 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_3 id: upcoming_date_3 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_3 id: upcoming_item_3 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_4 id: upcoming_date_4 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_4 id: upcoming_item_4 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_5 id: upcoming_date_5 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_5 id: upcoming_item_5 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_6 id: upcoming_date_6 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_6 id: upcoming_item_6 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_7 id: upcoming_date_7 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_7 id: upcoming_item_7 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_8 id: upcoming_date_8 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_8 id: upcoming_item_8 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_9 id: upcoming_date_9 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_9 id: upcoming_item_9 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_10 id: upcoming_date_10 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_10 id: upcoming_item_10 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_11 id: upcoming_date_11 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_11 id: upcoming_item_11 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_12 id: upcoming_date_12 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_12 id: upcoming_item_12 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_13 id: upcoming_date_13 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_13 id: upcoming_item_13 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_14 id: upcoming_date_14 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_14 id: upcoming_item_14 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_15 id: upcoming_date_15 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_15 id: upcoming_item_15 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_16 id: upcoming_date_16 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_16 id: upcoming_item_16 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_17 id: upcoming_date_17 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_17 id: upcoming_item_17 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_date_18 id: upcoming_date_18 on_value: then: - lambda: 'id(data_updated) = true;' - platform: homeassistant entity_id: '${ha_tasks_data}' attribute: upcoming_item_18 id: upcoming_item_18 on_value: then: - lambda: 'id(data_updated) = true;' # Now render everything on the ePaper screen. display: - platform: waveshare_epaper id: eink_display cs_pin: number: ${cs_pin} ignore_strapping_warning: true dc_pin: ${dc_pin} busy_pin: number: ${busy_pin} inverted: ${busy_inverted} reset_pin: ${reset_pin} model: ${model} update_interval: never setup_priority: 1000 rotation: 90° lambda: |- // Left from Madelena's code... perhaps if you want a black or colored background first (useful for fast-refresh screens?) to fill the background. // it.fill(color_bg); // Alignment rectangles! (uncomment to use - useful when first mounting the eInk screen) // it.rectangle(0, 0, 480, 800); // it.rectangle(10, 10, 460, 780); // it.rectangle(20, 20, 440, 760); // it.rectangle(30, 30, 420, 740); // it.rectangle(40, 40, 400, 720); // it.rectangle(50, 50, 380, 700); // it.rectangle(60, 60, 360, 680); // it.rectangle(70, 70, 340, 660); // it.rectangle(80, 80, 320, 640); // Because this list will dynamically space, we should define the starting column and row and row spacing (in pixels) // every time an item exists, spacing is added accordingly... center is hard-coded as 240 (assuming 480 width) int col = 27; int row = 68; // make the spacing the font size + a bit more int spacing_title = 64; int spacing_today = 30; int spacing_small_title = 36; int spacing_upcoming_date = 22; int spacing_upcoming_item = 28; int lastrow = 715 - spacing_upcoming_item - spacing_upcoming_date; int whiteout_row = 0; // this is used to make the left and right sides evenly blank, so the text doesn't look like its running under the frame // TODAY'S TASKS it.printf(240, row, id(font_title), color_red, TextAlign::TOP_CENTER, "TO DO"); row = row + spacing_title; whiteout_row = row; // The following code is not elegant. I realize it would have been more efficient to have a loop run on arrays and use delimiters. // But the possibility that the delimiter is actually in the task field was too great so I stuck with this one-by-one method if (! (id(today_0).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_0).state.c_str()); row = row + spacing_today; } else { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", "Nothing today!"); row = row + spacing_today; } if (! (id(today_1).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_1).state.c_str()); row = row + spacing_today; } if (! (id(today_2).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_2).state.c_str()); row = row + spacing_today; } if (! (id(today_3).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_3).state.c_str()); row = row + spacing_today; } if (! (id(today_4).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_4).state.c_str()); row = row + spacing_today; } if (! (id(today_5).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_5).state.c_str()); row = row + spacing_today; } if (! (id(today_6).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_6).state.c_str()); row = row + spacing_today; } if (! (id(today_7).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_7).state.c_str()); row = row + spacing_today; } if (! (id(today_8).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_8).state.c_str()); row = row + spacing_today; } if (! (id(today_9).state.empty())) { it.printf(col, row, id(font_today_list), color_blk, TextAlign::TOP_LEFT, "%s", id(today_9).state.c_str()); row = row + spacing_today; } it.filled_rectangle((it.get_width()-col), whiteout_row, (it.get_width()-col), row, COLOR_OFF); // UPCOMING TASKS row = row - spacing_today + (spacing_small_title); it.printf(240, row, id(font_small_title), color_red, TextAlign::TOP_CENTER, "UPCOMING"); row = row + spacing_small_title; whiteout_row = row; if ((row <= lastrow) && (! (id(upcoming_item_0).state.empty()))) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_0).state + ":").c_str()); row = row + spacing_upcoming_date; it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_0).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_1).state.empty()))) { if (id(upcoming_date_1).state.compare(id(upcoming_date_0).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_1).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_1).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_2).state.empty()))) { if (id(upcoming_date_2).state.compare(id(upcoming_date_1).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_2).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_2).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_3).state.empty()))) { if (id(upcoming_date_3).state.compare(id(upcoming_date_2).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_3).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_3).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_4).state.empty()))) { if (id(upcoming_date_4).state.compare(id(upcoming_date_3).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_4).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_4).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_5).state.empty()))) { if (id(upcoming_date_5).state.compare(id(upcoming_date_4).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_5).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_5).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_6).state.empty()))) { if (id(upcoming_date_6).state.compare(id(upcoming_date_5).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_6).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_6).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_7).state.empty()))) { if (id(upcoming_date_7).state.compare(id(upcoming_date_6).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_7).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_7).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_8).state.empty()))) { if (id(upcoming_date_8).state.compare(id(upcoming_date_7).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_8).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_8).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_9).state.empty()))) { if (id(upcoming_date_9).state.compare(id(upcoming_date_8).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_9).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_9).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_10).state.empty()))) { if (id(upcoming_date_10).state.compare(id(upcoming_date_9).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_10).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_10).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_11).state.empty()))) { if (id(upcoming_date_11).state.compare(id(upcoming_date_10).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_11).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_11).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_12).state.empty()))) { if (id(upcoming_date_12).state.compare(id(upcoming_date_11).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_12).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_12).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_13).state.empty()))) { if (id(upcoming_date_13).state.compare(id(upcoming_date_12).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_13).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_13).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_14).state.empty()))) { if (id(upcoming_date_14).state.compare(id(upcoming_date_13).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_14).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_14).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_15).state.empty()))) { if (id(upcoming_date_15).state.compare(id(upcoming_date_14).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_15).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_15).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_16).state.empty()))) { if (id(upcoming_date_16).state.compare(id(upcoming_date_15).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_16).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_16).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_17).state.empty()))) { if (id(upcoming_date_17).state.compare(id(upcoming_date_16).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_17).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_17).state).c_str()); row = row + spacing_upcoming_item; } if ((row <= lastrow) && (! (id(upcoming_item_18).state.empty()))) { if (id(upcoming_date_18).state.compare(id(upcoming_date_17).state) != 0) { it.printf(col, row, id(font_upcoming_date), color_blk, TextAlign::TOP_LEFT, "%s", (id(upcoming_date_18).state + ":").c_str()); row = row + spacing_upcoming_date; } it.printf(col, row, id(font_upcoming_item), color_blk, TextAlign::TOP_LEFT, "%s", (" " + id(upcoming_item_18).state).c_str()); row = row + spacing_upcoming_item; } it.filled_rectangle((it.get_width()-col), whiteout_row, (it.get_width()-col), row, COLOR_OFF); // Refresh Timestamp with time and day // from https://community.home-assistant.io/t/esphome-show-time/348903 char str[17]; time_t currTime = id(homeassistant_time).now().timestamp; strftime(str, sizeof(str), "%l:%M%p %a", localtime(&currTime)); it.printf(240, 715, id(font_refresh_stamp), color_blk, TextAlign::TOP_CENTER, "Refreshed at %s", str); // Battery Icon by Trip5 (placement at bottom right can be tricky) std::map battery_icon_map { {"batteryhigh", "\U000F12A3"}, {"batterymed", "\U000F12A2"}, {"batterylow", "\U000F12A1"}, {"batterywarn", "\U000F10CD"} }; int batt_x = 440; int batt_y = 712; float batt_v = float(id(battery_adc).state); if (0 <= batt_v && batt_v < 3.4) { it.printf(batt_x, batt_y, id(font_battery_glyphs), color_red, TextAlign::TOP_CENTER, "%s", battery_icon_map["batterywarn"].c_str()); } if (3.4 <= batt_v && batt_v < 3.6) { it.printf(batt_x, batt_y, id(font_battery_glyphs), color_red, TextAlign::TOP_CENTER, "%s", battery_icon_map["batterylow"].c_str()); } if (3.6 <= batt_v && batt_v < 3.8) { it.printf(batt_x, batt_y, id(font_battery_glyphs), color_blk, TextAlign::TOP_CENTER, "%s", battery_icon_map["batterymed"].c_str()); } if (3.8 <= batt_v && batt_v < 4.3) { it.printf(batt_x, batt_y, id(font_battery_glyphs), color_blk, TextAlign::TOP_CENTER, "%s", battery_icon_map["batteryhigh"].c_str()); } // battery icon not displayed if voltage is above 4.3V (but ADC must still be connected to 5V pin)