From 3da69c19ab62b0ec582ce265772d5e78c03acece Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 19:14:52 +0200 Subject: [PATCH 01/15] Base epaperframe config with Kinderzimmer --- esphome/epaperframe.yaml | 833 ++++++++++++++++++++++++++++++--------- 1 file changed, 644 insertions(+), 189 deletions(-) diff --git a/esphome/epaperframe.yaml b/esphome/epaperframe.yaml index eba92fd..e3485b5 100644 --- a/esphome/epaperframe.yaml +++ b/esphome/epaperframe.yaml @@ -15,7 +15,7 @@ esphome: priority: -10 then: - delay: 10s - - display.page.show: power # temporary for power page dev + - display.page.show: environment # temporary for power page dev - component.update: epaper esp32: @@ -23,13 +23,10 @@ esp32: framework: type: esp-idf -network: - enable_ipv6: true - wifi: ssid: "Voltage-legacy" password: !secret voltage_legacy_psk - #use_address: epaperframe.home + use_address: epaperframe.home power_save_mode: high fast_connect: on min_auth_mode: WPA2 @@ -81,7 +78,7 @@ binary_sensor: then: - logger.log: level: INFO - format: 'Button doubleclick, rendering next page!' + format: "Button doubleclick, rendering next page!" - display.page.show_next: epaper - component.update: epaper - timing: @@ -90,7 +87,7 @@ binary_sensor: then: - logger.log: level: INFO - format: 'Button click, refreshing screen!' + format: "Button click, refreshing screen!" - component.update: epaper # HA pushbutton configuration @@ -102,7 +99,7 @@ button: on_press: - logger.log: level: INFO - format: 'HA Button click, refreshing screen!' + format: "HA Button click, refreshing screen!" - component.update: epaper - platform: template name: Next screen @@ -111,7 +108,7 @@ button: on_press: - logger.log: level: INFO - format: 'HA Button click, rendering next page!' + format: "HA Button click, rendering next page!" - display.page.show_next: epaper - component.update: epaper @@ -122,10 +119,6 @@ sensor: id: wifisignal update_interval: 60s - - platform: homeassistant - entity_id: sensor.livingroom_wohnzimmer_feinstaub_pm_2_5_m - id: pm25_livingroom - - platform: homeassistant entity_id: sensor.wohnzimmer_temperatur id: temp_livingroom @@ -139,8 +132,8 @@ sensor: id: co2_livingroom - platform: homeassistant - entity_id: sensor.alpstuga_air_quality_monitor_pm25_2 - id: pm25_sleepingroom + entity_id: sensor.livingroom_wohnzimmer_feinstaub_pm_2_5_m + id: pm25_livingroom - platform: homeassistant entity_id: sensor.alpstuga_air_quality_monitor_temperatur_2 @@ -154,6 +147,26 @@ sensor: entity_id: sensor.alpstuga_air_quality_monitor_kohlendioxid_2 id: co2_sleepingroom + - platform: homeassistant + entity_id: sensor.alpstuga_air_quality_monitor_pm25_2 + id: pm25_sleepingroom + + - platform: homeassistant + entity_id: sensor.alpstuga_air_quality_monitor_temperatur + id: temp_childrensroom + + - platform: homeassistant + entity_id: sensor.alpstuga_air_quality_monitor_luftfeuchtigkeit + id: humid_childrensroom + + - platform: homeassistant + entity_id: sensor.alpstuga_air_quality_monitor_kohlendioxid + id: co2_childrensroom + + - platform: homeassistant + entity_id: sensor.alpstuga_air_quality_monitor_pm25 + id: pm25_childrensroom + - platform: homeassistant entity_id: sensor.aussentemperatur id: temp_outdoor @@ -189,11 +202,11 @@ sensor: - platform: homeassistant entity_id: sensor.schlafzimmer_bett_power id: bedlight_sleepingroom - + - platform: homeassistant entity_id: sensor.schlafzimmer_heimkino_power id: media_sleepingroom - + - platform: homeassistant entity_id: sensor.radioaktive_strahlungsleistung id: radiation @@ -205,7 +218,7 @@ text_sensor: entity_id: sensor.sun_rising_template id: sun_rising internal: true - + - platform: homeassistant name: "Sun Setting ESP" #entity_id: sensor.sun_next_setting @@ -221,22 +234,6 @@ text_sensor: entity_id: weather.zuhause id: weather - - platform: homeassistant - entity_id: sensor.schlafzimmer_co2_warnstufe - id: szco2 - - - platform: homeassistant - entity_id: sensor.schlafzimmer_feinstaub_warnstufe - id: szpm - - - platform: homeassistant - entity_id: sensor.wohnzimmer_co2_warnstufe - id: wzco2 - - - platform: homeassistant - entity_id: sensor.wohnzimmer_feinstaub_warnstufe - id: wzpm - graph: - id: line_power_graph sensor: power_total @@ -248,144 +245,476 @@ time: - platform: homeassistant id: homeassistant_time on_time: - # Every 10 seconds - - seconds: /10 - then: - - logger.log: - level: INFO - format: 'Power button press' - - output.turn_on: power_key - - delay: 0.5s - - output.turn_off: power_key - # Every 10 minutes - - seconds: 0 - minutes: /10 - then: - - logger.log: - level: INFO - format: 'Timer based screen refresh' - - component.update: epaper + # Every 10 seconds + - seconds: /10 + then: + - logger.log: + level: INFO + format: "Power button press" + - output.turn_on: power_key + - delay: 0.5s + - output.turn_off: power_key + # Every 10 minutes + - seconds: 0 + minutes: /10 + then: + - logger.log: + level: INFO + format: "Timer based screen refresh" + - component.update: epaper # Font definitions font: - - file: 'fonts/Roboto-Medium.ttf' + - file: "fonts/Roboto-Medium.ttf" id: roboto_med_30 size: 30 - - file: 'fonts/Roboto-Regular.ttf' + - file: "fonts/Roboto-Regular.ttf" id: footer_font size: 15 glyphs: - ['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', - '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', - 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', - 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f', - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', - 'u', 'v', 'w', 'x', 'y', 'z', 'Ä', 'ä', 'ö', '/', 'º', 'µ', '³', 'ß', - 'Ü', 'ü', 'Ö', '0'] + [ + "&", + "@", + "!", + ",", + ".", + '"', + "%", + "(", + ")", + "+", + "-", + "_", + ":", + "°", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + " ", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "Ä", + "ä", + "ö", + "/", + "º", + "µ", + "³", + "ß", + "Ü", + "ü", + "Ö", + "0", + ] - - file: 'fonts/Roboto-Regular.ttf' + - file: "fonts/Roboto-Regular.ttf" id: sensor_unit size: 20 glyphs: - ['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', - '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', - 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', - 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f', - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', - 'u', 'v', 'w', 'x', 'y', 'z', 'Ä', 'ä', 'ö', '/', 'º', 'µ', '³', 'ß', - 'Ü', 'ü', 'Ö', '0'] + [ + "&", + "@", + "!", + ",", + ".", + '"', + "%", + "(", + ")", + "+", + "-", + "_", + ":", + "°", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + " ", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "Ä", + "ä", + "ö", + "/", + "º", + "µ", + "³", + "ß", + "Ü", + "ü", + "Ö", + "0", + ] - - file: 'fonts/Roboto-Medium.ttf' + - file: "fonts/Roboto-Medium.ttf" id: sub_sensor_font size: 30 glyphs: - ['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', - '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', - 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', - 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f', - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', - 'u', 'v', 'w', 'x', 'y', 'z', 'Ä', 'ä', 'ö', '/', 'º', 'µ', '³', 'ß', - 'Ü', 'ü', 'Ö', '0'] + [ + "&", + "@", + "!", + ",", + ".", + '"', + "%", + "(", + ")", + "+", + "-", + "_", + ":", + "°", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + " ", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "Ä", + "ä", + "ö", + "/", + "º", + "µ", + "³", + "ß", + "Ü", + "ü", + "Ö", + "0", + ] - - file: 'fonts/Roboto-Bold.ttf' + - file: "fonts/Roboto-Bold.ttf" id: big_sensor_font size: 50 glyphs: - ['&', '@', '!', ',', '.', '"', '%', '(', ')', '+', '-', '_', ':', '°', - '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', - 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', - 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', 'a', 'b', 'c', 'd', 'e', 'f', - 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', - 'u', 'v', 'w', 'x', 'y', 'z', 'Ä', 'ä', 'ö', '/', 'º', 'µ', '³', 'ß', - 'Ü', 'ü', 'Ö', '0'] + [ + "&", + "@", + "!", + ",", + ".", + '"', + "%", + "(", + ")", + "+", + "-", + "_", + ":", + "°", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + " ", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "Ä", + "ä", + "ö", + "/", + "º", + "µ", + "³", + "ß", + "Ü", + "ü", + "Ö", + "0", + ] - - file: 'fonts/materialdesignicons-webfont.ttf' - id: mdi_small + - file: "fonts/materialdesignicons-webfont.ttf" + id: mdi_small size: 31 glyphs: [ - '󰔏', # mdi-thermometer - '󰖎', # mdi-water-percent - '󰮬', # mdi-molecule (pm2.5) - '󰟤', # mdi-co2 - '󰊚', # mdi-gauge (air pressure) - '󰖙', # mdi-weather-sunny - '󰖛', # mdi-weather-sunset-down - '󰖜', # mdi-weather-sunset-up - '󱐋', # mdi-lightning-bolt - '󰴾', # mdi-transmission-tower - '󰚥', # mdi-power-plug - '󱈹', # mdi-desk - '󰔂', # mdi-television - '󰐼' # mdi-radiation + "󰔏", # mdi-thermometer + "󰖎", # mdi-water-percent + "󰮬", # mdi-molecule (pm2.5) + "󰟤", # mdi-co2 + "󰊚", # mdi-gauge (air pressure) + "󰖙", # mdi-weather-sunny + "󰖛", # mdi-weather-sunset-down + "󰖜", # mdi-weather-sunset-up + "󱐋", # mdi-lightning-bolt + "󰴾", # mdi-transmission-tower + "󰚥", # mdi-power-plug + "󱈹", # mdi-desk + "󰔂", # mdi-television + "󰐼", # mdi-radiation ] - - file: 'fonts/materialdesignicons-webfont.ttf' - id: mdi_weather + - file: "fonts/materialdesignicons-webfont.ttf" + id: mdi_weather size: 80 glyphs: [ - '󰽡', # mdi-moon-first-quarter - '󰽢', # mdi-moon-full - '󰽣', # mdi-moon-last-quarter - '󰽤', # mdi-moon-new - '󰽥', # mdi-moon-waning-crescent - '󰽦', # mdi-moon-waning-gibbous - '󰽧', # mdi-moon-waxing-crescent - '󰽨', # mdi-moon-waxing-gibbous - '󰖔', # mdi-weather-night - '󰖐', # mdi-weather-cloudy - '󰖑', # mdi-weather-fog - '󰖒', # mdi-weather-hail - '󰖓', # mdi-weather-lightning - '󰙾', # mdi-weather-lightning-rainy - '󰖕', # mdi-weather-partly-cloudy - '󰖖', # mdi-weather-pouring - '󰖗', # mdi-weather-rainy - '󰖘', # mdi-weather-snowy - '󰙿', # mdi-weather-snowy-rainy - '󰖙', # mdi-weather-sunny - '󰖝', # mdi-weather-windy - '󰖞', # mdi-weather-windy-variant - '󰳥' # mdi-weather-exceptional + "󰽡", # mdi-moon-first-quarter + "󰽢", # mdi-moon-full + "󰽣", # mdi-moon-last-quarter + "󰽤", # mdi-moon-new + "󰽥", # mdi-moon-waning-crescent + "󰽦", # mdi-moon-waning-gibbous + "󰽧", # mdi-moon-waxing-crescent + "󰽨", # mdi-moon-waxing-gibbous + "󰖔", # mdi-weather-night + "󰖐", # mdi-weather-cloudy + "󰖑", # mdi-weather-fog + "󰖒", # mdi-weather-hail + "󰖓", # mdi-weather-lightning + "󰙾", # mdi-weather-lightning-rainy + "󰖕", # mdi-weather-partly-cloudy + "󰖖", # mdi-weather-pouring + "󰖗", # mdi-weather-rainy + "󰖘", # mdi-weather-snowy + "󰙿", # mdi-weather-snowy-rainy + "󰖙", # mdi-weather-sunny + "󰖝", # mdi-weather-windy + "󰖞", # mdi-weather-windy-variant + "󰳥", # mdi-weather-exceptional ] - - file: 'fonts/materialdesignicons-webfont.ttf' - id: mdi_wifi + - file: "fonts/materialdesignicons-webfont.ttf" + id: mdi_wifi size: 20 glyphs: [ - # WiFi - '󰤫', # mdi-wifi-strength-alert-outline - '󰤟', # mdi-wifi-strength-1 - '󰤢', # mdi-wifi-strength-2 - '󰤥', # mdi-wifi-strength-3 - '󰤨' # mdi-wifi-strength-4 + # WiFi + "󰤫", # mdi-wifi-strength-alert-outline + "󰤟", # mdi-wifi-strength-1 + "󰤢", # mdi-wifi-strength-2 + "󰤥", # mdi-wifi-strength-3 + "󰤨", # mdi-wifi-strength-4 ] - - file: 'fonts/materialdesignicons-webfont.ttf' - id: mdi_med + - file: "fonts/materialdesignicons-webfont.ttf" + id: mdi_med size: 60 glyphs: [ - '󰀦' # mdi-alert + "󰀦", # mdi-alert ] image: @@ -412,20 +741,29 @@ display: pages: - id: environment lambda: |- - + /* Outdoor */ float outdoor_temp = id(temp_outdoor).state; int outdoor_humid = int(id(humid_outdoor).state); float air_pressure = id(airpressure).state; float outdoor_radiation = id(radiation).state; + /* Living room */ float livingroom_temp = id(temp_livingroom).state; int livingroom_humid = int(id(humid_livingroom).state); - int livingroom_pm25 = int(id(pm25_livingroom).state); float livingroom_co2 = id(co2_livingroom).state; + int livingroom_pm25 = int(id(pm25_livingroom).state); + /* Serverroom */ + float serverroom_temp = id(temp_serverroom).state; + /* Sleeping room */ float sleepingroom_temp = id(temp_sleepingroom).state; int sleepingroom_humid = int(id(humid_sleepingroom).state); - int sleepingroom_pm25 = int(id(pm25_sleepingroom).state); float sleepingroom_co2 = id(co2_sleepingroom).state; - float serverroom_temp = id(temp_serverroom).state; + int sleepingroom_pm25 = int(id(pm25_sleepingroom).state); + /* Children's room */ + float childrensroom_temp = id(temp_childrensroom).state; + int childrensroom_humid = int(id(humid_childrensroom).state); + float childrensroom_co2 = id(co2_childrensroom).state; + int childrensroom_pm25 = int(id(pm25_childrensroom).state); + int offsetX = 0; int offsetY = 0; @@ -477,90 +815,89 @@ display: it.print(225, 130, id(sensor_unit), TextAlign::BASELINE_LEFT, "hPa"); } - /* Livingroom */ offsetY = 127; it.print(10, 10 + offsetY, id(sensor_unit), "Wohnzimmer"); - it.filled_rectangle(136, 21 + offsetY, 247, 3); + it.filled_rectangle(136, 21 + offsetY, 169, 3); it.print(10, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰮬"); // PM2.5 - it.print(375, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer - it.print(375, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent + it.print(305, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer + it.print(305, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent it.print(10, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰟤"); // CO2 if(livingroom_temp > -100 && livingroom_temp < 100) { - it.printf(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", livingroom_temp); + it.printf(287, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", livingroom_temp); } else { - it.print(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); + it.print(287, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); } if(livingroom_humid >=0 && livingroom_humid <= 100) { - it.printf(357, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", livingroom_humid); + it.printf(287, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", livingroom_humid); } else { - it.print(357, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %"); + it.print(287, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %"); } if(livingroom_co2 >=380 && livingroom_co2 <= 20000) { - it.printf(170, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", livingroom_co2); - it.print(175, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm"); + it.printf(150, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", livingroom_co2); + it.print(155, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm"); } else { - it.print(170, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); - it.print(175, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³"); + it.print(150, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); + it.print(155, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³"); } if(livingroom_pm25 < 255 && livingroom_pm25 >=0) { - it.printf(220, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", livingroom_pm25); - it.print(225, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); + it.printf(150, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", livingroom_pm25); + it.print(155, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); } else { - it.print(220, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); - it.print(225, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); + it.print(150, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); + it.print(155, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); } /* Sleepingroom */ offsetY = 247; it.print(10, 10 + offsetY, id(sensor_unit), "Schlafzimmer"); - it.filled_rectangle(144, 21 + offsetY, 239, 3); - it.print(10, 124 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰮬"); // PM2.5 - it.print(375, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer - it.print(375, 124 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent + it.filled_rectangle(144, 21 + offsetY, 161, 3); + it.print(10, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰮬"); // PM2.5 + it.print(305, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer + it.print(305, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent it.print(10, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰟤"); // CO2 if(sleepingroom_temp > -100 && sleepingroom_temp < 100) { - it.printf(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", sleepingroom_temp); + it.printf(287, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", sleepingroom_temp); } else { - it.print(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); + it.print(287, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); } if(sleepingroom_humid >=0 && sleepingroom_humid <= 100) { - it.printf(357, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", sleepingroom_humid); + it.printf(287, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", sleepingroom_humid); } else { - it.print(357, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %"); + it.print(287, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %"); } if(sleepingroom_co2 >=380 && sleepingroom_co2 <= 20000) { - it.printf(170, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", sleepingroom_co2); - it.print(175, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm"); + it.printf(150, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", sleepingroom_co2); + it.print(155, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm"); } else { - it.print(170, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); - it.print(175, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³"); + it.print(150, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); + it.print(155, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³"); } if(sleepingroom_pm25 < 255 && sleepingroom_pm25 >=0) { - it.printf(220, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", sleepingroom_pm25); - it.print(225, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); + it.printf(150, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", sleepingroom_pm25); + it.print(155, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); } else { - it.print(220, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); - it.print(225, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); + it.print(150, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); + it.print(155, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); } /* Weather */ @@ -571,7 +908,7 @@ display: /* Current weather */ if(id(weather).has_state()) { int x = 227 + offsetX; - int y = 116; + int y = 96; if (id(weather).state == "clear-night") { // clear night it.print(x, y, id(mdi_weather), TextAlign::BASELINE_RIGHT, "󰖔"); @@ -642,7 +979,7 @@ display: /* Moon phase display */ if(id(moonphase).has_state()) { int x = 10 + offsetX; - int y = 116; + int y = 96; if (id(moonphase).state == "new_moon") { // new moon it.print(x, y, id(mdi_weather), TextAlign::BASELINE_LEFT, "󰽤"); @@ -682,30 +1019,66 @@ display: } } - /* Warnings */ - if(id(szco2).state == "rot" || id(szpm).state == "rot" || id(wzco2).state == "rot" || id(wzpm).state == "rot") { - it.print(10 + offsetX, 200, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); - it.print(80 + offsetX, 200, id(big_sensor_font), TextAlign::BASELINE_LEFT, "Lüften!"); + it.print(10 + offsetX, 130, id(mdi_small), TextAlign::BASELINE_LEFT, "󰖜"); // sun-up + it.printf(50 + offsetX, 130, id(sensor_unit), TextAlign::BASELINE_LEFT, "%s", id(sun_rising).state.c_str()); + it.print(135 + offsetX, 130, id(mdi_small), TextAlign::BASELINE_LEFT, "󰖛"); // sun-down + it.printf(175 + offsetX, 130, id(sensor_unit), TextAlign::BASELINE_LEFT, "%s", id(sun_setting).state.c_str()); + + /* Children's room */ + offsetX = 320; + offsetY = 127; + it.print(10 + offsetX, 10 + offsetY, id(sensor_unit), "Kinderzimmer"); + it.filled_rectangle(148 + offsetX, 21 + offsetY, 157, 3); + it.print(10 + offsetX, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰮬"); // PM2.5 + it.print(305 + offsetX, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer + it.print(305 + offsetX, 125 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰖎"); // water percent + it.print(10 + offsetX, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_LEFT, "󰟤"); // CO2 + + if(childrensroom_temp > -100 && childrensroom_temp < 100) { + it.printf(287 + offsetX, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1fºC", childrensroom_temp); + } + else { + it.print(287 + offsetX, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); } - it.print(10 + offsetX, 250, id(mdi_small), TextAlign::BASELINE_LEFT, "󰖜"); // sun-up - it.printf(50 + offsetX, 250, id(sensor_unit), TextAlign::BASELINE_LEFT, "%s", id(sun_rising).state.c_str()); - it.print(135 + offsetX, 250, id(mdi_small), TextAlign::BASELINE_LEFT, "󰖛"); // sun-down - it.printf(175 + offsetX, 250, id(sensor_unit), TextAlign::BASELINE_LEFT, "%s", id(sun_setting).state.c_str()); + if(childrensroom_humid >=0 && childrensroom_humid <= 100) { + it.printf(287 + offsetX, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%3d%%", childrensroom_humid); + } + else { + it.print(287 + offsetX, 125 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - %"); + } + + if(childrensroom_co2 >=380 && childrensroom_co2 <= 20000) { + it.printf(150 + offsetX, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%4.0f", childrensroom_co2); + it.print(155 + offsetX, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm"); + } + else { + it.print(150 + offsetX, 75 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); + it.print(155 + offsetX, 50 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "ppm³"); + } + + if(childrensroom_pm25 < 255 && childrensroom_pm25 >=0) { + it.printf(150 + offsetX, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%3d", childrensroom_pm25); + it.print(155 + offsetX, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); + } + else { + it.print(150 + offsetX, 125 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "- "); + it.print(155 + offsetX, 125 + offsetY, id(sensor_unit), TextAlign::BASELINE_LEFT, "µg/m³"); + } /* Serverroom */ offsetY = 247; it.print(10 + offsetX, 10 + offsetY, id(sensor_unit), "Serverraum"); - it.filled_rectangle(125 + offsetX, 21 + offsetY, 110, 3); + it.filled_rectangle(125 + offsetX, 21 + offsetY, 180, 3); - it.print(227 + offsetX, 110 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer + it.print(305 + offsetX, 110 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰔏"); // thermometer if(serverroom_temp > -100 && serverroom_temp < 100) { - it.printf(207 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "ºC"); - it.printf(170 + offsetX, 110 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1f", serverroom_temp); + it.printf(287 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "ºC"); + it.printf(250 + offsetX, 110 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1f", serverroom_temp); } else { - it.print(207 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); + it.print(287 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - ºC"); } /* FOOTER */ @@ -737,6 +1110,88 @@ display: ESP_LOGI("WiFi", "Unlikely"); } } + - id: calendar + lambda: |- + /* Fetch today and tomorrow dates */ + auto now = id(homeassistant_time).now(); + int today_day = now.day_of_month; + int today_month = now.month; + int today_wday = now.day_of_week; // 0=Sunday .. 6=Saturday + const char* weekdays[] = {"So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"}; + const char* today_weekday_str = weekdays[today_wday]; + int today_year = now.year; + + /* Tomorrow's date */ + int tomorrow_day = today_day + 1; + int tomorrow_month = today_month; + int tomorrow_wday = (today_wday + 1) % 7; + const char* tomorrow_weekday_str = weekdays[tomorrow_wday]; + int tomorrow_year = today_year; + + /* C1024 logo */ + it.image(10, 37, id(c1024_logo)); + + /* ── LEFT PANE: Today ── */ + int leftX = 0; + int leftY = 85; // start below header row + + it.print(10 + leftX, 10 + leftY, id(sensor_unit), "Heute"); + it.filled_rectangle(64 + leftX, 21 + leftY, 370, 3); + + /* Today's date strip */ + it.printf(50 + leftX, 50 + leftY, id(big_sensor_font), "%d", today_day); + it.printf(130 + leftX, 45 + leftY, id(sub_sensor_font), "%s.", today_weekday_str); + it.printf(160 + leftX, 45 + leftY, id(sensor_unit), "%02d.%04d.", today_month, today_year); + + /* Event placeholder row (top) */ + int eventOffsetY = 90; + it.print(10 + leftX, 50 + leftY + eventOffsetY, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); + it.print(40 + leftX, 50 + leftY + eventOffsetY, id(sub_sensor_font), "Eintrag 1"); + + /* Event placeholder row (bottom) */ + it.print(10 + leftX, 85 + leftY + eventOffsetY, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); + it.print(40 + leftX, 85 + leftY + eventOffsetY, id(sub_sensor_font), "Eintrag 2"); + + /* ── RIGHT PANE: Tomorrow ── */ + int rightX = 390; + int rightY = 85; + + it.print(10 + rightX, 10 + rightY, id(sensor_unit), "Morgen"); + it.filled_rectangle(64 + rightX, 21 + rightY, 370, 3); + + /* Date strip */ + it.printf(50 + rightX, 50 + rightY, id(big_sensor_font), "%d", tomorrow_day); + it.printf(130 + rightX, 45 + rightY, id(sub_sensor_font), "%s.", tomorrow_weekday_str); + it.printf(160 + rightX, 45 + rightY, id(sensor_unit), "%02d.%04d.", tomorrow_month, tomorrow_year); + + /* Event placeholder row (top) */ + it.print(10 + rightX, 50 + rightY + eventOffsetY, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); + it.print(40 + rightX, 50 + rightY + eventOffsetY, id(sub_sensor_font), "Eintrag 1"); + + /* Event placeholder row (bottom) */ + it.print(10 + rightX, 85 + rightY + eventOffsetY, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); + it.print(40 + rightX, 85 + rightY + eventOffsetY, id(sub_sensor_font), "Eintrag 2"); + + /* FOOTER */ + it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT, "Aktualisiert um %d.%m.%Y %H:%M", id(homeassistant_time).now()); + + /* WiFi Signal strength */ + if(id(wifisignal).has_state()) { + int x = 630; + int y = 384; + if (id(wifisignal).state >= -50) { + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤨"); + } else if (id(wifisignal).state >= -60) { + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤥"); + } else if (id(wifisignal).state >= -75) { + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤢"); + } else if (id(wifisignal).state >= -100) { + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤟"); + } else { + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤫"); + } + } + - id: power lambda: |- @@ -763,7 +1218,7 @@ display: offsetY = 127; it.print(10, 10 + offsetY, id(sensor_unit), "Wohnzimmer"); it.filled_rectangle(136, 21 + offsetY, 247, 3); - + it.print(375, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug it.print(375, 115 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug @@ -834,7 +1289,7 @@ display: /* FOOTER */ it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT , "Aktualisiert um %d.%m.%Y %H:%M", id(homeassistant_time).now()); - /* WiFi Signal Strenght */ + /* WiFi Signal strength */ if(id(wifisignal).has_state()) { int x = 630; int y = 384; From 38da2cebddb7d24dae04b83403e214300b1cffb4 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 21:00:42 +0200 Subject: [PATCH 02/15] Remove epaper power display components --- esphome/epaperframe.yaml | 234 +-------------------------------------- 1 file changed, 1 insertion(+), 233 deletions(-) diff --git a/esphome/epaperframe.yaml b/esphome/epaperframe.yaml index e3485b5..51472c8 100644 --- a/esphome/epaperframe.yaml +++ b/esphome/epaperframe.yaml @@ -183,29 +183,7 @@ sensor: entity_id: sensor.serverroom_serveraum_temperatur id: temp_serverroom - - platform: homeassistant - entity_id: sensor.netzleistung - id: power_total - - platform: homeassistant - entity_id: sensor.serverraum_power - id: power_serverroom - - - platform: homeassistant - entity_id: sensor.schreibtisch_power - id: desk_livingroom - - - platform: homeassistant - entity_id: sensor.wohnzimmer_heimkino_power - id: media_livingroom - - - platform: homeassistant - entity_id: sensor.schlafzimmer_bett_power - id: bedlight_sleepingroom - - - platform: homeassistant - entity_id: sensor.schlafzimmer_heimkino_power - id: media_sleepingroom - platform: homeassistant entity_id: sensor.radioaktive_strahlungsleistung @@ -234,12 +212,7 @@ text_sensor: entity_id: weather.zuhause id: weather -graph: - - id: line_power_graph - sensor: power_total - duration: 2h - width: 235 - height: 100 + time: - platform: homeassistant @@ -1110,208 +1083,3 @@ display: ESP_LOGI("WiFi", "Unlikely"); } } - - id: calendar - lambda: |- - /* Fetch today and tomorrow dates */ - auto now = id(homeassistant_time).now(); - int today_day = now.day_of_month; - int today_month = now.month; - int today_wday = now.day_of_week; // 0=Sunday .. 6=Saturday - const char* weekdays[] = {"So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"}; - const char* today_weekday_str = weekdays[today_wday]; - int today_year = now.year; - - /* Tomorrow's date */ - int tomorrow_day = today_day + 1; - int tomorrow_month = today_month; - int tomorrow_wday = (today_wday + 1) % 7; - const char* tomorrow_weekday_str = weekdays[tomorrow_wday]; - int tomorrow_year = today_year; - - /* C1024 logo */ - it.image(10, 37, id(c1024_logo)); - - /* ── LEFT PANE: Today ── */ - int leftX = 0; - int leftY = 85; // start below header row - - it.print(10 + leftX, 10 + leftY, id(sensor_unit), "Heute"); - it.filled_rectangle(64 + leftX, 21 + leftY, 370, 3); - - /* Today's date strip */ - it.printf(50 + leftX, 50 + leftY, id(big_sensor_font), "%d", today_day); - it.printf(130 + leftX, 45 + leftY, id(sub_sensor_font), "%s.", today_weekday_str); - it.printf(160 + leftX, 45 + leftY, id(sensor_unit), "%02d.%04d.", today_month, today_year); - - /* Event placeholder row (top) */ - int eventOffsetY = 90; - it.print(10 + leftX, 50 + leftY + eventOffsetY, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); - it.print(40 + leftX, 50 + leftY + eventOffsetY, id(sub_sensor_font), "Eintrag 1"); - - /* Event placeholder row (bottom) */ - it.print(10 + leftX, 85 + leftY + eventOffsetY, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); - it.print(40 + leftX, 85 + leftY + eventOffsetY, id(sub_sensor_font), "Eintrag 2"); - - /* ── RIGHT PANE: Tomorrow ── */ - int rightX = 390; - int rightY = 85; - - it.print(10 + rightX, 10 + rightY, id(sensor_unit), "Morgen"); - it.filled_rectangle(64 + rightX, 21 + rightY, 370, 3); - - /* Date strip */ - it.printf(50 + rightX, 50 + rightY, id(big_sensor_font), "%d", tomorrow_day); - it.printf(130 + rightX, 45 + rightY, id(sub_sensor_font), "%s.", tomorrow_weekday_str); - it.printf(160 + rightX, 45 + rightY, id(sensor_unit), "%02d.%04d.", tomorrow_month, tomorrow_year); - - /* Event placeholder row (top) */ - it.print(10 + rightX, 50 + rightY + eventOffsetY, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); - it.print(40 + rightX, 50 + rightY + eventOffsetY, id(sub_sensor_font), "Eintrag 1"); - - /* Event placeholder row (bottom) */ - it.print(10 + rightX, 85 + rightY + eventOffsetY, id(mdi_med), TextAlign::BASELINE_LEFT, "󰀦"); - it.print(40 + rightX, 85 + rightY + eventOffsetY, id(sub_sensor_font), "Eintrag 2"); - - /* FOOTER */ - it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT, "Aktualisiert um %d.%m.%Y %H:%M", id(homeassistant_time).now()); - - /* WiFi Signal strength */ - if(id(wifisignal).has_state()) { - int x = 630; - int y = 384; - if (id(wifisignal).state >= -50) { - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤨"); - } else if (id(wifisignal).state >= -60) { - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤥"); - } else if (id(wifisignal).state >= -75) { - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤢"); - } else if (id(wifisignal).state >= -100) { - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤟"); - } else { - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤫"); - } - } - - - id: power - lambda: |- - - float total_power = id(power_total).state; - float serverroom_power = id(power_serverroom).state; - float livingroom_desk = id(desk_livingroom).state; - float livingroom_media = id(media_livingroom).state; - float sleepingroom = id(media_sleepingroom).state + id(bedlight_sleepingroom).state; - int offsetX = 0; - int offsetY = 0; - - /* BASEMENT */ - it.print(10, 10, id(sensor_unit), "Keller"); - /* - Draw the outline of a rectangle with the top left at [86,21], - a width of 297 and a height of 3 - */ - it.filled_rectangle(76, 21, 307, 3); - - /* C1024 logo */ - it.image(10 , 37, id(c1024_logo)); - - /* Livingroom */ - offsetY = 127; - it.print(10, 10 + offsetY, id(sensor_unit), "Wohnzimmer"); - it.filled_rectangle(136, 21 + offsetY, 247, 3); - - it.print(375, 75 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug - it.print(375, 115 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug - - it.print(20, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_LEFT, "Schreibtisch"); - if(livingroom_desk >= 0) { - it.printf(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1f W", livingroom_desk); - } - else { - it.print(357, 75 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - W"); - } - - it.print(20, 115 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_LEFT, "Heimkino"); - if(livingroom_media >= 0) { - it.printf(357, 115 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1f W", livingroom_media); - } - else { - it.print(357, 115 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - W"); - } - - /* Sleepingroom */ - offsetY = 247; - it.print(10, 10 + offsetY, id(sensor_unit), "Schlafzimmer"); - it.filled_rectangle(144, 21 + offsetY, 239, 3); - - it.print(375, 115 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug - - // it.print(20, 115 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_LEFT, "Heimkino"); - if(sleepingroom >= 0) { - it.printf(357, 115 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1f W", sleepingroom); - } - else { - it.print(357, 115 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - W"); - } - - /* LINEPOWER */ - offsetX = 390; - offsetY = 65; - it.print(10 + offsetX, 10, id(sensor_unit), "Gesamt"); - it.filled_rectangle(90 + offsetX, 21, 155, 3); - - it.print(227 + offsetX, 20 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰴾"); // transmission-tower - - if(total_power >= 0) { - it.printf(207 + offsetX, 20 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "W"); - it.printf(170 + offsetX, 20 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1f", total_power); - } - else { - it.print(207 + offsetX, 20 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - W"); - } - - it.graph(10 + offsetX, 30 + offsetY, id(line_power_graph)); - - /* Serverroom */ - offsetY = 247; - it.print(10 + offsetX, 10 + offsetY, id(sensor_unit), "Serverraum"); - it.filled_rectangle(125 + offsetX, 21 + offsetY, 110, 3); - - it.print(227 + offsetX, 110 + offsetY, id(mdi_small), TextAlign::BASELINE_CENTER, "󰚥"); // power-plug - - if(serverroom_power >= 0) { - it.printf(207 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, "W"); - it.printf(170 + offsetX, 110 + offsetY, id(big_sensor_font), TextAlign::BASELINE_RIGHT, "%2.1f", serverroom_power); - } - else { - it.print(207 + offsetX, 110 + offsetY, id(sub_sensor_font), TextAlign::BASELINE_RIGHT, " - W"); - } - - /* FOOTER */ - it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT , "Aktualisiert um %d.%m.%Y %H:%M", id(homeassistant_time).now()); - - /* WiFi Signal strength */ - if(id(wifisignal).has_state()) { - int x = 630; - int y = 384; - if (id(wifisignal).state >= -50) { - // Excellent - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤨"); - ESP_LOGI("WiFi", "Excellent"); - } else if (id(wifisignal).state >= -60) { - // Good - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤥"); - ESP_LOGI("WiFi", "Good"); - } else if (id(wifisignal).state >= -75) { - // Fair - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤢"); - ESP_LOGI("WiFi", "Fair"); - } else if (id(wifisignal).state >= -100) { - // Weak - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤟"); - ESP_LOGI("WiFi", "Weak"); - } else { - // Unlikely working signal - it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤫"); - ESP_LOGI("WiFi", "Unlikely"); - } - } From 3b7c30634e4d42cf18a7c2f173da954c6f50fce5 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 21:41:21 +0200 Subject: [PATCH 03/15] Add compact calendar feed to e-paper display + HA --- configuration.yaml | 3 + esphome/epaperframe.yaml | 172 +++++++++++++++++++++++++ python_scripts/epaper_calendar_data.py | 84 ++++++++++++ template.yaml | 30 +++++ 4 files changed, 289 insertions(+) create mode 100644 python_scripts/epaper_calendar_data.py diff --git a/configuration.yaml b/configuration.yaml index 56cfcf1..a1e04cc 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -35,6 +35,9 @@ utility_meter: !include utility_meters.yaml mqtt: !include mqtt.yaml mqtt_statestream: !include mqtt_statestream.yaml +# Calendar data is normalised in a sandboxed script before ESPHome receives it. +python_script: + # Template sensors template: !include template.yaml diff --git a/esphome/epaperframe.yaml b/esphome/epaperframe.yaml index 51472c8..da9ec46 100644 --- a/esphome/epaperframe.yaml +++ b/esphome/epaperframe.yaml @@ -212,6 +212,12 @@ text_sensor: entity_id: weather.zuhause id: weather + # Display-ready JSON produced by the Home Assistant calendar template sensor. + - platform: homeassistant + entity_id: sensor.epaper_kalenderdaten + attribute: entries + id: calendar_entries + time: @@ -1083,3 +1089,169 @@ display: ESP_LOGI("WiFi", "Unlikely"); } } + + - id: calendar + lambda: |- + auto now = id(homeassistant_time).now(); + const char *months[] = {"Januar", "Februar", "März", "April", "Mai", "Juni", + "Juli", "August", "September", "Oktober", "November", "Dezember"}; + const char *weekdays[] = {"Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"}; + + const int year = now.year; + const int month = now.month; + const int today = now.day_of_month; + + auto days_in_month = [](int calendar_year, int calendar_month) { + const int days[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (calendar_month == 2) { + return (calendar_year % 4 == 0 && (calendar_year % 100 != 0 || calendar_year % 400 == 0)) ? 29 : 28; + } + return days[calendar_month - 1]; + }; + auto weekday_monday = [](int calendar_year, int calendar_month, int day) { + const int offsets[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4}; + calendar_year -= calendar_month < 3; + const int sunday_first = (calendar_year + calendar_year / 4 - calendar_year / 100 + + calendar_year / 400 + offsets[calendar_month - 1] + day) % 7; + return (sunday_first + 6) % 7; + }; + auto iso_weeks_in_year = [&weekday_monday](int calendar_year) { + // 28 December is always in the final ISO week of its year. + const int day_of_year = 362 + ((calendar_year % 4 == 0 && + (calendar_year % 100 != 0 || calendar_year % 400 == 0)) ? 1 : 0); + const int weekday = weekday_monday(calendar_year, 12, 28) + 1; + return (day_of_year - weekday + 10) / 7; + }; + auto iso_week = [&weekday_monday, &days_in_month, &iso_weeks_in_year](int calendar_year, int calendar_month, int day) { + int day_of_year = day; + for (int current_month = 1; current_month < calendar_month; current_month++) { + day_of_year += days_in_month(calendar_year, current_month); + } + const int weekday = weekday_monday(calendar_year, calendar_month, day) + 1; + int week = (day_of_year - weekday + 10) / 7; + if (week < 1) return iso_weeks_in_year(calendar_year - 1); + if (week > iso_weeks_in_year(calendar_year)) return 1; + return week; + }; + + /* LEFT PANE: agenda */ + it.image(10, 10, id(c1024_logo)); + it.print(100, 27, id(roboto_med_30), TextAlign::BASELINE_LEFT, "TERMINE"); + it.line(10, 55, 355, 55); + it.line(365, 10, 365, 352); + + DynamicJsonDocument calendar_doc(3072); + const DeserializationError calendar_error = deserializeJson(calendar_doc, id(calendar_entries).state.c_str()); + JsonArray calendar_days = calendar_doc.as(); + int agenda_y = 72; + bool has_events = false; + bool has_hidden_events = false; + + if (calendar_error || calendar_days.isNull()) { + it.print(18, agenda_y, id(sensor_unit), TextAlign::TOP_LEFT, "Kalenderdaten werden geladen ..."); + } else if (calendar_days.size() == 0) { + it.print(18, agenda_y, id(sensor_unit), TextAlign::TOP_LEFT, "Keine Termine in den nächsten Tagen."); + } else { + for (JsonVariant day_variant : calendar_days) { + JsonObject day = day_variant.as(); + JsonArray events = day["events"].as(); + if (events.isNull() || events.size() == 0) continue; + if (agenda_y + 38 > 345) { + has_hidden_events = true; + break; + } + + const int day_number = day["day"] | 0; + const char *weekday = day["weekday"] | ""; + const bool is_today = (day["today"] | 0) == 1; + it.printf(30, agenda_y, id(sub_sensor_font), TextAlign::TOP_CENTER, "%d", day_number); + it.printf(30, agenda_y + 28, id(footer_font), TextAlign::TOP_CENTER, "%s", weekday); + it.print(68, agenda_y + 2, id(footer_font), TextAlign::TOP_LEFT, is_today ? "HEUTE" : ""); + it.line(58, agenda_y, 58, agenda_y + 32 + events.size() * 35); + agenda_y += 36; + + for (JsonVariant event_variant : events) { + if (agenda_y + 33 > 345) { + has_hidden_events = true; + break; + } + JsonObject event = event_variant.as(); + const char *title = event["title"] | "Ohne Titel"; + const char *time = event["time"] | ""; + const char *meta = event["meta"] | ""; + it.printf(72, agenda_y, id(sensor_unit), TextAlign::TOP_LEFT, "%s", title); + it.printf(350, agenda_y, id(sensor_unit), TextAlign::TOP_RIGHT, "%s", time); + it.printf(72, agenda_y + 20, id(footer_font), TextAlign::TOP_LEFT, "%s", meta); + agenda_y += 35; + has_events = true; + } + agenda_y += 5; + if (has_hidden_events) break; + } + } + + if (!has_events && !calendar_error && !calendar_days.isNull() && calendar_days.size() > 0) { + it.print(18, 72, id(sensor_unit), TextAlign::TOP_LEFT, "Keine Termine im sichtbaren Bereich."); + } + if (has_hidden_events) { + it.print(72, 340, id(footer_font), TextAlign::TOP_LEFT, "Weitere Termine vorhanden ..."); + } + + /* RIGHT TOP: month view, Monday-first with ISO week numbers */ + char month_title[24]; + snprintf(month_title, sizeof(month_title), "%s %d", months[month - 1], year); + it.printf(502, 18, id(sensor_unit), TextAlign::TOP_CENTER, "%s", month_title); + it.line(375, 28, 630, 28); + it.print(384, 47, id(footer_font), TextAlign::TOP_CENTER, "KW"); + for (int column = 0; column < 7; column++) { + it.printf(415 + column * 30, 47, id(footer_font), TextAlign::TOP_CENTER, "%s", weekdays[column]); + } + + const int first_weekday = weekday_monday(year, month, 1); + const int month_days = days_in_month(year, month); + for (int row = 0; row < 6; row++) { + const int monday_day = 1 - first_weekday + row * 7; + int week_year = year; + int week_month = month; + int week_day = monday_day; + if (week_day < 1) { + week_month--; + if (week_month == 0) { + week_month = 12; + week_year--; + } + week_day += days_in_month(week_year, week_month); + } else if (week_day > month_days) { + week_day -= month_days; + week_month++; + if (week_month == 13) { + week_month = 1; + week_year++; + } + } + const int cell_y = 69 + row * 23; + it.printf(384, cell_y, id(footer_font), TextAlign::TOP_CENTER, "%02d", iso_week(week_year, week_month, week_day)); + + for (int column = 0; column < 7; column++) { + const int day = row * 7 + column - first_weekday + 1; + if (day < 1 || day > month_days) continue; + const int cell_x = 415 + column * 30; + if (day == today) { + it.filled_rectangle(cell_x - 12, cell_y - 2, 25, 18, Color::BLACK); + it.printf(cell_x, cell_y, id(footer_font), Color::WHITE, TextAlign::TOP_CENTER, "%d", day); + } else { + it.printf(cell_x, cell_y, id(footer_font), TextAlign::TOP_CENTER, "%d", day); + } + } + } + + /* RIGHT BOTTOM: quote placeholder, intentionally inverted */ + it.filled_rectangle(375, 220, 255, 130, Color::BLACK); + it.print(502, 234, id(footer_font), Color::WHITE, TextAlign::TOP_CENTER, "DEMOTIVATION"); + it.print(502, 267, id(sensor_unit), Color::WHITE, TextAlign::TOP_CENTER, "Es ist nie zu spät,"); + it.print(502, 291, id(sensor_unit), Color::WHITE, TextAlign::TOP_CENTER, "die Erwartungen zu senken."); + it.print(502, 326, id(footer_font), Color::WHITE, TextAlign::TOP_CENTER, "Zitatquelle folgt"); + + /* FOOTER */ + it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT, + "Aktualisiert um %d.%m.%Y %H:%M", now); diff --git a/python_scripts/epaper_calendar_data.py b/python_scripts/epaper_calendar_data.py new file mode 100644 index 0000000..222e670 --- /dev/null +++ b/python_scripts/epaper_calendar_data.py @@ -0,0 +1,84 @@ +# Calendar data is compacted here so the ESP32 only receives display-ready entries. +CALENDAR_NAMES = { + "calendar.tkrz_kalender": "Arbeit", + "calendar.privat": "Privat", +} +DAY_NAMES = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] +MAX_ENTRIES = 8 +MAX_TITLE_LENGTH = 28 +MAX_META_LENGTH = 32 + + +def shorten(value, maximum): + value = (value or "").strip() + if len(value) <= maximum: + return value + return value[: maximum - 3].rstrip() + "..." + + +def compact_event(event, calendar_name): + start = event.get("start", "") + all_day = "T" not in start + time = "ganztägig" if all_day else start[11:16] + + location = (event.get("location") or "").split("\n")[0].strip() + meta = calendar_name + if location: + meta += " / " + location + + return { + "title": shorten(event.get("summary", "Ohne Titel"), MAX_TITLE_LENGTH), + "time": time, + "meta": shorten(meta, MAX_META_LENGTH), + "all_day": int(all_day), + } + + +calendar_data = data.get("calendar", {}) +today = str(data.get("now", ""))[:10] +events_by_date = {} + +for calendar_id, calendar in calendar_data.items(): + calendar_name = CALENDAR_NAMES.get(calendar_id, calendar_id.split(".")[-1].capitalize()) + for event in calendar.get("events", []): + start = event.get("start", "") + end = event.get("end", "") + event_date = start[:10] + end_date = end[:10] + + # Ignore malformed or already finished entries. All-day event end dates + # are exclusive in Home Assistant; timed events ending today may still be + # active. Multi-day entries remain visible on today's date rail. + all_day = "T" not in start + if not event_date or (all_day and end_date <= today) or (not all_day and end_date < today): + continue + if event_date < today: + event_date = today + + events_by_date.setdefault(event_date, []).append( + compact_event(event, calendar_name) + ) + +entries = [] +entry_count = 0 +for date in sorted(events_by_date): + if entry_count >= MAX_ENTRIES: + break + + events = events_by_date[date] + events.sort(key=lambda item: (item["all_day"] == 0, item["time"])) + remaining = MAX_ENTRIES - entry_count + events = events[:remaining] + entry_count += len(events) + + parsed_date = dt_util.parse_datetime(date) + entries.append( + { + "day": parsed_date.day, + "weekday": DAY_NAMES[parsed_date.weekday()], + "today": int(date == today), + "events": events, + } + ) + +output["entries"] = entries diff --git a/template.yaml b/template.yaml index 18b08ee..f347d7a 100644 --- a/template.yaml +++ b/template.yaml @@ -49,6 +49,7 @@ name: Sun Setting Template state: '{{ as_timestamp(states.sun.sun.attributes.next_setting) | timestamp_custom (''%H:%M'') }}' + - name: "power_other" unique_id: '5579422933393' unit_of_measurement: "W" @@ -79,6 +80,35 @@ attributes: last_reset: '1970-01-01T00:00:00+00:00' + # Compact calendar feed for the e-paper display. The actual event data lives in + # the `entries` attribute, which ESPHome reads as a text sensor. + - trigger: + - trigger: homeassistant + event: start + - trigger: time_pattern + minutes: /15 + action: + - action: calendar.get_events + target: + entity_id: + - calendar.tkrz_kalender + - calendar.privat + data: + duration: + days: 14 + response_variable: calendar_response + - action: python_script.epaper_calendar_data + data: + calendar: "{{ calendar_response }}" + now: "{{ now().date() }}" + response_variable: calendar_converted + sensor: + - name: Epaper Kalenderdaten + unique_id: epaper_calendar_data + state: "{{ calendar_converted.entries | count }}" + attributes: + entries: "{{ calendar_converted.entries }}" + # Shelly 3EM cumulative sensors (incl. PV) - sensor: # Template sensor for values of power import (active_power > 0) From 6149a9e9942206c864c664cf58f9edc992186543 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 22:23:45 +0200 Subject: [PATCH 04/15] Add calendar styling and WiFi indicator --- esphome/epaperframe.yaml | 55 ++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/esphome/epaperframe.yaml b/esphome/epaperframe.yaml index da9ec46..c35a688 100644 --- a/esphome/epaperframe.yaml +++ b/esphome/epaperframe.yaml @@ -15,7 +15,7 @@ esphome: priority: -10 then: - delay: 10s - - display.page.show: environment # temporary for power page dev + - display.page.show: calendar # temporary for power page dev - component.update: epaper esp32: @@ -46,6 +46,9 @@ api: encryption: key: !secret apikey +# Include ArduinoJson support for the compact calendar event feed. +json: + ota: platform: esphome password: !secret ota @@ -248,6 +251,12 @@ font: id: roboto_med_30 size: 30 + # Small, digits-only font used to emphasize the current date in the month grid. + - file: "fonts/Roboto-Bold.ttf" + id: calendar_day_bold + size: 15 + glyphs: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + - file: "fonts/Roboto-Regular.ttf" id: footer_font size: 15 @@ -1136,7 +1145,7 @@ display: /* LEFT PANE: agenda */ it.image(10, 10, id(c1024_logo)); - it.print(100, 27, id(roboto_med_30), TextAlign::BASELINE_LEFT, "TERMINE"); + it.print(226, 38, id(roboto_med_30), TextAlign::BASELINE_CENTER, "TERMINE"); it.line(10, 55, 355, 55); it.line(365, 10, 365, 352); @@ -1201,7 +1210,6 @@ display: char month_title[24]; snprintf(month_title, sizeof(month_title), "%s %d", months[month - 1], year); it.printf(502, 18, id(sensor_unit), TextAlign::TOP_CENTER, "%s", month_title); - it.line(375, 28, 630, 28); it.print(384, 47, id(footer_font), TextAlign::TOP_CENTER, "KW"); for (int column = 0; column < 7; column++) { it.printf(415 + column * 30, 47, id(footer_font), TextAlign::TOP_CENTER, "%s", weekdays[column]); @@ -1237,8 +1245,8 @@ display: if (day < 1 || day > month_days) continue; const int cell_x = 415 + column * 30; if (day == today) { - it.filled_rectangle(cell_x - 12, cell_y - 2, 25, 18, Color::BLACK); - it.printf(cell_x, cell_y, id(footer_font), Color::WHITE, TextAlign::TOP_CENTER, "%d", day); + it.filled_rectangle(cell_x - 12, cell_y - 2, 25, 18, COLOR_ON); + it.printf(cell_x, cell_y, id(calendar_day_bold), COLOR_OFF, TextAlign::TOP_CENTER, "%d", day); } else { it.printf(cell_x, cell_y, id(footer_font), TextAlign::TOP_CENTER, "%d", day); } @@ -1246,12 +1254,39 @@ display: } /* RIGHT BOTTOM: quote placeholder, intentionally inverted */ - it.filled_rectangle(375, 220, 255, 130, Color::BLACK); - it.print(502, 234, id(footer_font), Color::WHITE, TextAlign::TOP_CENTER, "DEMOTIVATION"); - it.print(502, 267, id(sensor_unit), Color::WHITE, TextAlign::TOP_CENTER, "Es ist nie zu spät,"); - it.print(502, 291, id(sensor_unit), Color::WHITE, TextAlign::TOP_CENTER, "die Erwartungen zu senken."); - it.print(502, 326, id(footer_font), Color::WHITE, TextAlign::TOP_CENTER, "Zitatquelle folgt"); + it.filled_rectangle(375, 220, 255, 130, COLOR_ON); + it.print(502, 246, id(sensor_unit), COLOR_OFF, TextAlign::TOP_CENTER, "Es ist nie zu spät,"); + it.print(502, 271, id(sensor_unit), COLOR_OFF, TextAlign::TOP_CENTER, "die Erwartungen zu"); + it.print(502, 296, id(sensor_unit), COLOR_OFF, TextAlign::TOP_CENTER, "senken."); + it.print(502, 326, id(footer_font), COLOR_OFF, TextAlign::TOP_CENTER, "Zitatquelle folgt"); /* FOOTER */ it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT, "Aktualisiert um %d.%m.%Y %H:%M", now); + + /* WiFi Signal Strenght */ + if(id(wifisignal).has_state()) { + int x = 630; + int y = 384; + if (id(wifisignal).state >= -50) { + // Excellent + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤨"); + ESP_LOGI("WiFi", "Excellent"); + } else if (id(wifisignal).state >= -60) { + // Good + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤥"); + ESP_LOGI("WiFi", "Good"); + } else if (id(wifisignal).state >= -75) { + // Fair + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤢"); + ESP_LOGI("WiFi", "Fair"); + } else if (id(wifisignal).state >= -100) { + // Weak + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤟"); + ESP_LOGI("WiFi", "Weak"); + } else { + // Unlikely working signal + it.print(x, y, id(mdi_wifi), TextAlign::BASELINE_CENTER, "󰤫"); + ESP_LOGI("WiFi", "Unlikely"); + } + } From ee814c4afee84e987fcbaf02687cdb4c2cfd8039 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 22:26:08 +0200 Subject: [PATCH 05/15] Refine calendar event data display, prevent overlapping and display event's end time. --- esphome/epaperframe.yaml | 30 +++++++++++++++----------- python_scripts/epaper_calendar_data.py | 8 +++++-- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/esphome/epaperframe.yaml b/esphome/epaperframe.yaml index c35a688..8134bd0 100644 --- a/esphome/epaperframe.yaml +++ b/esphome/epaperframe.yaml @@ -1170,31 +1170,37 @@ display: break; } + const int group_y = agenda_y; + int event_y = group_y; const int day_number = day["day"] | 0; const char *weekday = day["weekday"] | ""; - const bool is_today = (day["today"] | 0) == 1; - it.printf(30, agenda_y, id(sub_sensor_font), TextAlign::TOP_CENTER, "%d", day_number); - it.printf(30, agenda_y + 28, id(footer_font), TextAlign::TOP_CENTER, "%s", weekday); - it.print(68, agenda_y + 2, id(footer_font), TextAlign::TOP_LEFT, is_today ? "HEUTE" : ""); - it.line(58, agenda_y, 58, agenda_y + 32 + events.size() * 35); - agenda_y += 36; + it.printf(30, group_y, id(sub_sensor_font), TextAlign::TOP_CENTER, "%d", day_number); + it.printf(30, group_y + 28, id(footer_font), TextAlign::TOP_CENTER, "%s", weekday); for (JsonVariant event_variant : events) { - if (agenda_y + 33 > 345) { + if (event_y + 33 > 345) { has_hidden_events = true; break; } JsonObject event = event_variant.as(); const char *title = event["title"] | "Ohne Titel"; const char *time = event["time"] | ""; + const char *end_time = event["end"] | ""; const char *meta = event["meta"] | ""; - it.printf(72, agenda_y, id(sensor_unit), TextAlign::TOP_LEFT, "%s", title); - it.printf(350, agenda_y, id(sensor_unit), TextAlign::TOP_RIGHT, "%s", time); - it.printf(72, agenda_y + 20, id(footer_font), TextAlign::TOP_LEFT, "%s", meta); - agenda_y += 35; + it.printf(72, event_y, id(sensor_unit), TextAlign::TOP_LEFT, "%s", title); + it.printf(350, event_y, id(sensor_unit), TextAlign::TOP_RIGHT, "%s", time); + it.printf(72, event_y + 20, id(footer_font), TextAlign::TOP_LEFT, "%s", meta); + if (end_time[0] != '\0') { + it.printf(350, event_y + 20, id(footer_font), TextAlign::TOP_RIGHT, "%s", end_time); + } + event_y += 35; has_events = true; } - agenda_y += 5; + + int group_bottom = event_y; + if (group_bottom < group_y + 36) group_bottom = group_y + 36; + it.line(58, group_y, 58, group_bottom); + agenda_y = group_bottom + 5; if (has_hidden_events) break; } } diff --git a/python_scripts/epaper_calendar_data.py b/python_scripts/epaper_calendar_data.py index 222e670..b5e38f5 100644 --- a/python_scripts/epaper_calendar_data.py +++ b/python_scripts/epaper_calendar_data.py @@ -5,8 +5,9 @@ CALENDAR_NAMES = { } DAY_NAMES = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] MAX_ENTRIES = 8 -MAX_TITLE_LENGTH = 28 -MAX_META_LENGTH = 32 +# Reserve the right side of each row for start and end times. +MAX_TITLE_LENGTH = 19 +MAX_META_LENGTH = 28 def shorten(value, maximum): @@ -19,7 +20,9 @@ def shorten(value, maximum): def compact_event(event, calendar_name): start = event.get("start", "") all_day = "T" not in start + end = event.get("end", "") time = "ganztägig" if all_day else start[11:16] + end_time = "" if all_day or "T" not in end else end[11:16] location = (event.get("location") or "").split("\n")[0].strip() meta = calendar_name @@ -29,6 +32,7 @@ def compact_event(event, calendar_name): return { "title": shorten(event.get("summary", "Ohne Titel"), MAX_TITLE_LENGTH), "time": time, + "end": end_time, "meta": shorten(meta, MAX_META_LENGTH), "all_day": int(all_day), } From 13e58355da85f162a96bd4311594e3f8f243af4e Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 22:39:15 +0200 Subject: [PATCH 06/15] Finetuning of available space --- esphome/epaperframe.yaml | 3 ++- python_scripts/epaper_calendar_data.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/esphome/epaperframe.yaml b/esphome/epaperframe.yaml index 8134bd0..55c58f0 100644 --- a/esphome/epaperframe.yaml +++ b/esphome/epaperframe.yaml @@ -1223,7 +1223,8 @@ display: const int first_weekday = weekday_monday(year, month, 1); const int month_days = days_in_month(year, month); - for (int row = 0; row < 6; row++) { + const int calendar_rows = (first_weekday + month_days + 6) / 7; + for (int row = 0; row < calendar_rows; row++) { const int monday_day = 1 - first_weekday + row * 7; int week_year = year; int week_month = month; diff --git a/python_scripts/epaper_calendar_data.py b/python_scripts/epaper_calendar_data.py index b5e38f5..47f73de 100644 --- a/python_scripts/epaper_calendar_data.py +++ b/python_scripts/epaper_calendar_data.py @@ -6,8 +6,8 @@ CALENDAR_NAMES = { DAY_NAMES = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] MAX_ENTRIES = 8 # Reserve the right side of each row for start and end times. -MAX_TITLE_LENGTH = 19 -MAX_META_LENGTH = 28 +MAX_TITLE_LENGTH = 25 +MAX_META_LENGTH = 35 def shorten(value, maximum): From c342efc08fea5404da1cfe6087abd3f5edaf61f6 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 22:47:32 +0200 Subject: [PATCH 07/15] Remove hidden-events footer from epaper frame --- esphome/epaperframe.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/epaperframe.yaml b/esphome/epaperframe.yaml index 55c58f0..40bea55 100644 --- a/esphome/epaperframe.yaml +++ b/esphome/epaperframe.yaml @@ -1149,7 +1149,7 @@ display: it.line(10, 55, 355, 55); it.line(365, 10, 365, 352); - DynamicJsonDocument calendar_doc(3072); + JsonDocument calendar_doc; const DeserializationError calendar_error = deserializeJson(calendar_doc, id(calendar_entries).state.c_str()); JsonArray calendar_days = calendar_doc.as(); int agenda_y = 72; @@ -1208,9 +1208,7 @@ display: if (!has_events && !calendar_error && !calendar_days.isNull() && calendar_days.size() > 0) { it.print(18, 72, id(sensor_unit), TextAlign::TOP_LEFT, "Keine Termine im sichtbaren Bereich."); } - if (has_hidden_events) { - it.print(72, 340, id(footer_font), TextAlign::TOP_LEFT, "Weitere Termine vorhanden ..."); - } + /* RIGHT TOP: month view, Monday-first with ISO week numbers */ char month_title[24]; From 250bfa94bd61139089d55cedec1bd309eb6d01fb Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 22:58:23 +0200 Subject: [PATCH 08/15] Add daily e-paper quote rotation --- automations.yaml | 19 +++++++++++++++++++ configuration.yaml | 3 +++ esphome/epaperframe.yaml | 30 +++++++++++++++++++++++++----- input_select.yaml | 40 ++++++++++++++++++++++++++++++++++++++++ template.yaml | 4 ++++ 5 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 input_select.yaml diff --git a/automations.yaml b/automations.yaml index 02c9895..6d6f99b 100644 --- a/automations.yaml +++ b/automations.yaml @@ -1438,3 +1438,22 @@ device_id: 6d1be741876624a70ab5b01b54c6fd6f entity_id: 621e4377a089b7f5d92c7c9f2cc171a1 domain: switch + +- id: epaper_daily_demotivational_quote + alias: Epaper tägliches Demotivationszitat + description: Wählt täglich ein festes Zitat aus der lokalen Zitatbibliothek aus. + triggers: + - trigger: homeassistant + event: start + - trigger: time + at: "00:00:00" + conditions: [] + actions: + - action: input_select.select_option + target: + entity_id: input_select.epaper_demotivationszitat + data: + option: >- + {% set quotes = state_attr('input_select.epaper_demotivationszitat', 'options') %} + {{ quotes[(now().strftime('%j') | int - 1) % (quotes | count)] }} + mode: single diff --git a/configuration.yaml b/configuration.yaml index a1e04cc..1506ed9 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -38,6 +38,9 @@ mqtt_statestream: !include mqtt_statestream.yaml # Calendar data is normalised in a sandboxed script before ESPHome receives it. python_script: +# Home Assistant-managed e-paper quote library +input_select: !include input_select.yaml + # Template sensors template: !include template.yaml diff --git a/esphome/epaperframe.yaml b/esphome/epaperframe.yaml index 40bea55..c1fbbfd 100644 --- a/esphome/epaperframe.yaml +++ b/esphome/epaperframe.yaml @@ -221,6 +221,11 @@ text_sensor: attribute: entries id: calendar_entries + # Home Assistant selects one quote per day from its local quote library. + - platform: homeassistant + entity_id: sensor.epaper_demotivationszitat + id: demotivational_quote + time: @@ -1258,12 +1263,27 @@ display: } } - /* RIGHT BOTTOM: quote placeholder, intentionally inverted */ + /* RIGHT BOTTOM: daily quote selected by Home Assistant */ it.filled_rectangle(375, 220, 255, 130, COLOR_ON); - it.print(502, 246, id(sensor_unit), COLOR_OFF, TextAlign::TOP_CENTER, "Es ist nie zu spät,"); - it.print(502, 271, id(sensor_unit), COLOR_OFF, TextAlign::TOP_CENTER, "die Erwartungen zu"); - it.print(502, 296, id(sensor_unit), COLOR_OFF, TextAlign::TOP_CENTER, "senken."); - it.print(502, 326, id(footer_font), COLOR_OFF, TextAlign::TOP_CENTER, "Zitatquelle folgt"); + std::string quote = "Zitat wird geladen."; + if (id(demotivational_quote).has_state() && !id(demotivational_quote).state.empty()) { + quote = id(demotivational_quote).state; + } + const size_t max_quote_characters = 27; + size_t quote_start = 0; + for (int line = 0; line < 4 && quote_start < quote.length(); line++) { + size_t quote_end = quote_start + max_quote_characters; + if (quote_end < quote.length()) { + const size_t word_break = quote.rfind(' ', quote_end); + if (word_break != std::string::npos && word_break > quote_start) quote_end = word_break; + } else { + quote_end = quote.length(); + } + const std::string quote_line = quote.substr(quote_start, quote_end - quote_start); + it.printf(502, 234 + line * 24, id(sensor_unit), COLOR_OFF, TextAlign::TOP_CENTER, "%s", quote_line.c_str()); + quote_start = quote_end; + while (quote_start < quote.length() && quote[quote_start] == ' ') quote_start++; + } /* FOOTER */ it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT, diff --git a/input_select.yaml b/input_select.yaml new file mode 100644 index 0000000..9852ea0 --- /dev/null +++ b/input_select.yaml @@ -0,0 +1,40 @@ +epaper_demotivationszitat: + name: Epaper Demotivationszitat + icon: mdi:emoticon-sad-outline + options: + - "Erwarte nichts. Dann kann dich auch wenig enttäuschen." + - "Du musst nicht scheitern. Es reicht, es nie zu versuchen." + - "Der Weg ist das Ziel. Leider führt er im Kreis." + - "Auch ein Rückschritt ist Bewegung. Nur eben rückwärts." + - "Nicht jeder Tag muss gut sein. Dieser hier zum Beispiel nicht." + - "Dein Potenzial ist beeindruckend. Seine Nutzung bleibt optional." + - "Morgen wird alles besser. Wahrscheinlich aber auch nicht." + - "Motivation ist vergänglich. Frust ist zuverlässig." + - "Man kann nicht alles haben. Nicht einmal gute Laune." + - "Wenn Plan A scheitert, war Plan B vermutlich auch keine Hilfe." + - "Es ist nie zu spät, die Erwartungen zu senken." + - "Du bist nicht zu spät. Die anderen sind nur früher gescheitert." + - "Das wird schon. Nur eben nicht gut." + - "Ziele sind wichtig. Besonders wenn man sie verfehlt." + - "Die Konkurrenz schläft nicht. Du darfst trotzdem weitermachen." + - "Du gibst dein Bestes. Das erklärt einiges." + - "Jeder Fehler ist eine Lektion. Du bist bald Experte." + - "Heute ist ein neuer Tag. Die Probleme sind dieselben." + - "Erfolg braucht Geduld. Enttäuschung ist sofort verfügbar." + - "Ein kleiner Schritt für dich. Ein kaum bemerkbarer für alle anderen." + - "Wer früh aufgibt, hat länger frei." + - "Es gibt keine dummen Fragen. Nur ungünstige Zeitpunkte." + - "Deine Komfortzone vermisst dich nicht." + - "Manchmal gewinnt man. Meistens lernt man daraus." + - "Der erste Schritt ist der schwerste. Die anderen sind nur länger." + - "Arbeite hart. Vielleicht merkt es niemand." + - "Nicht aufgeben. Es könnte noch schlimmer werden." + - "Träume groß. Die Realität übernimmt den Rest." + - "Das Glas ist halb voll. Mit Problemen." + - "Jeder Tag bietet Chancen. Manche davon sind Termine." + - "Du kannst alles schaffen. Außer vielleicht heute." + - "Das Leben ist kurz. Besprechungen sind länger." + - "Mut steht am Anfang. Die Rechnung kommt später." + - "Wenn es einfach wäre, würde es jemand anders machen." + - "Deine To-do-Liste glaubt an dich. Sie muss ja nicht mitmachen." + - "Perfektion ist erreichbar. Nur nicht von hier." diff --git a/template.yaml b/template.yaml index f347d7a..f5bed7e 100644 --- a/template.yaml +++ b/template.yaml @@ -49,6 +49,10 @@ name: Sun Setting Template state: '{{ as_timestamp(states.sun.sun.attributes.next_setting) | timestamp_custom (''%H:%M'') }}' + - unique_id: epaper_demotivational_quote + default_entity_id: sensor.epaper_demotivationszitat + name: Epaper Demotivationszitat + state: "{{ states('input_select.epaper_demotivationszitat') }}" - name: "power_other" unique_id: '5579422933393' From 99579a30f7a081cdc22c2a7fd438d0c548f57ffb Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 23:19:48 +0200 Subject: [PATCH 09/15] Updated HA. --- .HA_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.HA_VERSION b/.HA_VERSION index 3f77693..2398eb2 100644 --- a/.HA_VERSION +++ b/.HA_VERSION @@ -1 +1 @@ -2026.7.1 \ No newline at end of file +2026.7.2 \ No newline at end of file From bf49633facfda6188e91593edf9e59afe955b9c4 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Sat, 11 Jul 2026 23:20:09 +0200 Subject: [PATCH 10/15] Shortened calendar title again... --- python_scripts/epaper_calendar_data.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python_scripts/epaper_calendar_data.py b/python_scripts/epaper_calendar_data.py index 47f73de..28f5aea 100644 --- a/python_scripts/epaper_calendar_data.py +++ b/python_scripts/epaper_calendar_data.py @@ -1,4 +1,3 @@ -# Calendar data is compacted here so the ESP32 only receives display-ready entries. CALENDAR_NAMES = { "calendar.tkrz_kalender": "Arbeit", "calendar.privat": "Privat", @@ -6,7 +5,7 @@ CALENDAR_NAMES = { DAY_NAMES = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"] MAX_ENTRIES = 8 # Reserve the right side of each row for start and end times. -MAX_TITLE_LENGTH = 25 +MAX_TITLE_LENGTH = 22 MAX_META_LENGTH = 35 From e77d9ee8d29d0edb5d20f93d34baefd0d60a88cc Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 13 Jul 2026 08:23:31 +0200 Subject: [PATCH 11/15] Adopted VyOS SNMP router traffic counter. --- sensors.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sensors.yaml b/sensors.yaml index ae88765..31df4f6 100644 --- a/sensors.yaml +++ b/sensors.yaml @@ -10,7 +10,8 @@ unique_id: '3303381540758' host: !secret router_ip community: !secret router_community - baseoid: .1.3.6.1.2.1.31.1.1.1.6.511 +# baseoid: .1.3.6.1.2.1.31.1.1.1.6.511 + baseoid: .1.3.6.1.2.1.2.2.1.10.24 version: 2c unit_of_measurement: "Octets" - platform: snmp @@ -18,7 +19,8 @@ unique_id: '1573258703922' host: !secret router_ip community: !secret router_community - baseoid: .1.3.6.1.2.1.31.1.1.1.10.511 +# baseoid: .1.3.6.1.2.1.31.1.1.1.10.511 + baseoid: .1.3.6.1.2.1.2.2.1.16.24 version: 2c unit_of_measurement: "Octets" From c1ba3012d4feb75b42d0033a405e6faed0da4a97 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 13 Jul 2026 08:36:46 +0200 Subject: [PATCH 12/15] Fix traffic counters? --- utility_meters.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/utility_meters.yaml b/utility_meters.yaml index 9c91178..af3a4c2 100644 --- a/utility_meters.yaml +++ b/utility_meters.yaml @@ -1,31 +1,31 @@ # Internet traffic internet_usage_in_monthly: - source: sensor.snmp_wan_in + source: sensor.wan_in_derivative name: Monthly internet traffic in unique_id: monthly_internet_traffic_in cycle: monthly internet_usage_out_monthly: - source: sensor.snmp_wan_out + source: sensor.wan_out_derivative name: Monthly internet traffic out unique_id: monthly_internet_traffic_out cycle: monthly internet_usage_in_daily: - source: sensor.snmp_wan_in + source: sensor.wan_in_derivative name: Daily internet traffic in unique_id: daily_internet_traffic_in cycle: daily internet_usage_out_daily: - source: sensor.snmp_wan_out + source: sensor.wan_out_derivative name: Daily internet traffic out unique_id: daily_internet_traffic_out cycle: daily internet_usage_in_hourly: - source: sensor.snmp_wan_in + source: sensor.wan_in_derivative name: Hourly internet traffic in unique_id: hourly_internet_traffic_in cycle: hourly internet_usage_out_hourly: - source: sensor.snmp_wan_out + source: sensor.wan_out_derivative name: Hourly internet traffic out unique_id: hourly_internet_traffic_out cycle: hourly From 96b88624d997a6b655d40ac74dc78d72302172a9 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 13 Jul 2026 08:45:37 +0200 Subject: [PATCH 13/15] Revert "Fix traffic counters?" This reverts commit c1ba3012d4feb75b42d0033a405e6faed0da4a97. --- utility_meters.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/utility_meters.yaml b/utility_meters.yaml index af3a4c2..9c91178 100644 --- a/utility_meters.yaml +++ b/utility_meters.yaml @@ -1,31 +1,31 @@ # Internet traffic internet_usage_in_monthly: - source: sensor.wan_in_derivative + source: sensor.snmp_wan_in name: Monthly internet traffic in unique_id: monthly_internet_traffic_in cycle: monthly internet_usage_out_monthly: - source: sensor.wan_out_derivative + source: sensor.snmp_wan_out name: Monthly internet traffic out unique_id: monthly_internet_traffic_out cycle: monthly internet_usage_in_daily: - source: sensor.wan_in_derivative + source: sensor.snmp_wan_in name: Daily internet traffic in unique_id: daily_internet_traffic_in cycle: daily internet_usage_out_daily: - source: sensor.wan_out_derivative + source: sensor.snmp_wan_out name: Daily internet traffic out unique_id: daily_internet_traffic_out cycle: daily internet_usage_in_hourly: - source: sensor.wan_in_derivative + source: sensor.snmp_wan_in name: Hourly internet traffic in unique_id: hourly_internet_traffic_in cycle: hourly internet_usage_out_hourly: - source: sensor.wan_out_derivative + source: sensor.snmp_wan_out name: Hourly internet traffic out unique_id: hourly_internet_traffic_out cycle: hourly From 9104cd8fdc7d9eab66862b670db555d65f3b73a2 Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 13 Jul 2026 10:30:47 +0200 Subject: [PATCH 14/15] Reapply "Fix traffic counters?" This reverts commit 96b88624d997a6b655d40ac74dc78d72302172a9. --- utility_meters.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/utility_meters.yaml b/utility_meters.yaml index 9c91178..af3a4c2 100644 --- a/utility_meters.yaml +++ b/utility_meters.yaml @@ -1,31 +1,31 @@ # Internet traffic internet_usage_in_monthly: - source: sensor.snmp_wan_in + source: sensor.wan_in_derivative name: Monthly internet traffic in unique_id: monthly_internet_traffic_in cycle: monthly internet_usage_out_monthly: - source: sensor.snmp_wan_out + source: sensor.wan_out_derivative name: Monthly internet traffic out unique_id: monthly_internet_traffic_out cycle: monthly internet_usage_in_daily: - source: sensor.snmp_wan_in + source: sensor.wan_in_derivative name: Daily internet traffic in unique_id: daily_internet_traffic_in cycle: daily internet_usage_out_daily: - source: sensor.snmp_wan_out + source: sensor.wan_out_derivative name: Daily internet traffic out unique_id: daily_internet_traffic_out cycle: daily internet_usage_in_hourly: - source: sensor.snmp_wan_in + source: sensor.wan_in_derivative name: Hourly internet traffic in unique_id: hourly_internet_traffic_in cycle: hourly internet_usage_out_hourly: - source: sensor.snmp_wan_out + source: sensor.wan_out_derivative name: Hourly internet traffic out unique_id: hourly_internet_traffic_out cycle: hourly From 99bb2594e243fef292cf1dd1ba545eee4f4b830a Mon Sep 17 00:00:00 2001 From: Commander1024 Date: Mon, 13 Jul 2026 13:08:24 +0200 Subject: [PATCH 15/15] Fixed utility meters and unsmoothen internet bandwidth monitors. --- sensors.yaml | 8 ++++---- utility_meters.yaml | 6 ++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/sensors.yaml b/sensors.yaml index 31df4f6..03da1c1 100644 --- a/sensors.yaml +++ b/sensors.yaml @@ -11,7 +11,7 @@ host: !secret router_ip community: !secret router_community # baseoid: .1.3.6.1.2.1.31.1.1.1.6.511 - baseoid: .1.3.6.1.2.1.2.2.1.10.24 + baseoid: .1.3.6.1.2.1.2.2.1.10.25 version: 2c unit_of_measurement: "Octets" - platform: snmp @@ -20,7 +20,7 @@ host: !secret router_ip community: !secret router_community # baseoid: .1.3.6.1.2.1.31.1.1.1.10.511 - baseoid: .1.3.6.1.2.1.2.2.1.16.24 + baseoid: .1.3.6.1.2.1.2.2.1.16.25 version: 2c unit_of_measurement: "Octets" @@ -40,13 +40,13 @@ unique_id: '9081721471264' state_characteristic: mean entity_id: sensor.internet_speed_in - sampling_size: 10 + sampling_size: 2 - platform: statistics name: 'WAN Traffic Out' unique_id: '8688955223027' state_characteristic: mean entity_id: sensor.internet_speed_out - sampling_size: 10 + sampling_size: 2 # Sensor for Riemann sum of energy import (W -> Wh) - platform: integration diff --git a/utility_meters.yaml b/utility_meters.yaml index af3a4c2..109e3a0 100644 --- a/utility_meters.yaml +++ b/utility_meters.yaml @@ -4,31 +4,37 @@ name: Monthly internet traffic in unique_id: monthly_internet_traffic_in cycle: monthly + delta_values: True internet_usage_out_monthly: source: sensor.wan_out_derivative name: Monthly internet traffic out unique_id: monthly_internet_traffic_out cycle: monthly + delta_values: True internet_usage_in_daily: source: sensor.wan_in_derivative name: Daily internet traffic in unique_id: daily_internet_traffic_in cycle: daily + delta_values: True internet_usage_out_daily: source: sensor.wan_out_derivative name: Daily internet traffic out unique_id: daily_internet_traffic_out cycle: daily + delta_values: True internet_usage_in_hourly: source: sensor.wan_in_derivative name: Hourly internet traffic in unique_id: hourly_internet_traffic_in cycle: hourly + delta_values: True internet_usage_out_hourly: source: sensor.wan_out_derivative name: Hourly internet traffic out unique_id: hourly_internet_traffic_out cycle: hourly + delta_values: True # Energy energy_import_daily: