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 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 56cfcf1..1506ed9 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -35,6 +35,12 @@ 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: + +# 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 e3485b5..a92c714 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 @@ -183,29 +186,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 +215,18 @@ text_sensor: entity_id: weather.zuhause id: weather -graph: - - id: line_power_graph - sensor: power_total - duration: 2h - width: 235 - height: 100 + # Display-ready JSON produced by the Home Assistant calendar template sensor. + - platform: homeassistant + entity_id: sensor.epaper_kalenderdaten + 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: - platform: homeassistant @@ -269,6 +256,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 @@ -1110,184 +1103,191 @@ 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; + 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"}; - /* 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; + const int year = now.year; + const int month = now.month; + const int today = now.day_of_month; - /* C1024 logo */ - it.image(10, 37, id(c1024_logo)); + 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: Today ── */ - int leftX = 0; - int leftY = 85; // start below header row + /* LEFT PANE: agenda */ + it.image(10, 10, id(c1024_logo)); + it.print(226, 38, id(roboto_med_30), TextAlign::BASELINE_CENTER, "TERMINE"); + it.line(10, 55, 355, 55); + it.line(365, 10, 365, 352); - it.print(10 + leftX, 10 + leftY, id(sensor_unit), "Heute"); - it.filled_rectangle(64 + leftX, 21 + leftY, 370, 3); + 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; + bool has_events = false; + bool has_hidden_events = false; - /* 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); + 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; + } - /* 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"); + const int group_y = agenda_y; + int event_y = group_y; + const int day_number = day["day"] | 0; + const char *weekday = day["weekday"] | ""; + 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); - /* 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"); + for (JsonVariant event_variant : events) { + 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, 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; + } - /* ── 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, "󰤫"); + 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; } } - - 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"); + 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."); } - 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"); + + /* 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.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]); } - /* Sleepingroom */ - offsetY = 247; - it.print(10, 10 + offsetY, id(sensor_unit), "Schlafzimmer"); - it.filled_rectangle(144, 21 + offsetY, 239, 3); + const int first_weekday = weekday_monday(year, month, 1); + const int month_days = days_in_month(year, month); + 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; + 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)); - 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"); + 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_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); + } + } } - /* 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); + /* RIGHT BOTTOM: daily quote selected by Home Assistant */ + it.filled_rectangle(375, 220, 255, 130, COLOR_ON); + std::string quote = "Zitat wird geladen."; + if (id(demotivational_quote).has_state() && !id(demotivational_quote).state.empty()) { + quote = id(demotivational_quote).state; } - 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"); + 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 , "Aktualisiert um %d.%m.%Y %H:%M", id(homeassistant_time).now()); + it.strftime(614, 380, id(footer_font), TextAlign::BASELINE_RIGHT, + "Aktualisiert um %d.%m.%Y %H:%M", now); /* WiFi Signal strength */ if(id(wifisignal).has_state()) { 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/python_scripts/epaper_calendar_data.py b/python_scripts/epaper_calendar_data.py new file mode 100644 index 0000000..28f5aea --- /dev/null +++ b/python_scripts/epaper_calendar_data.py @@ -0,0 +1,87 @@ +CALENDAR_NAMES = { + "calendar.tkrz_kalender": "Arbeit", + "calendar.privat": "Privat", +} +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 = 22 +MAX_META_LENGTH = 35 + + +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 + 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 + if location: + meta += " / " + location + + 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), + } + + +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/sensors.yaml b/sensors.yaml index ae88765..03da1c1 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.25 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.25 version: 2c unit_of_measurement: "Octets" @@ -38,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/template.yaml b/template.yaml index 18b08ee..f5bed7e 100644 --- a/template.yaml +++ b/template.yaml @@ -49,6 +49,11 @@ 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' unit_of_measurement: "W" @@ -79,6 +84,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) diff --git a/utility_meters.yaml b/utility_meters.yaml index 9c91178..109e3a0 100644 --- a/utility_meters.yaml +++ b/utility_meters.yaml @@ -1,34 +1,40 @@ # 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 + delta_values: True 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 + delta_values: True 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 + delta_values: True 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 + delta_values: True 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 + delta_values: True 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 + delta_values: True # Energy energy_import_daily: