Skip to content

Deep sleep never updating display #276

@tristankin

Description

@tristankin

I have been trying to work out what is going on with the deep sleep and not getting updated stats. I think it is because the api feeds the info from home assistant and because HA cant see the device in deep sleep it backs off sending through the updated values.

I think to enable deep sleep mode the device needs to be changed from API to mqtt?

Also I don't think the code for silent hours runs when in deep sleep mode?

# ============================================================================
# ESPHome YAML - Generated by ESPHome Designer
# ============================================================================
# TARGET DEVICE: Seeed Studio Trmnl DIY Kit (ESP32-S3)
#         - Display Platform: waveshare_epaper
#         - PSRAM: Yes
#         - Touchscreen: No
#         - Framework: esp-idf (Recommended)
# ============================================================================
#
# SETUP INSTRUCTIONS:
#
# STEP 1: Copy the Material Design Icons font file
#         - From this repo: resources/fonts/materialdesignicons-webfont.ttf
#         - To ESPHome: /config/esphome/fonts/materialdesignicons-webfont.ttf
#
# STEP 2: Create a new device in ESPHome
#         - Click "New Device"
#         - Select: ESP32-S3 (or appropriate for your board)
#         - Framework: ESP-IDF (Essential for S3 stability)
#
# STEP 3: PASTE this snippet into your device YAML
#         - Paste this snippet at the end of your configuration.
#         - System sections (esphome, esp32, psram) are auto-commented
#           to avoid conflicts with your existing base setup.
#
# CAPTIVE PORTAL:
#         - If WiFi connection fails, the device will create a hotspot.
#         - Search for its name in your WiFi settings.
#         - Connect and go to http://192.168.4.1 to configure WiFi.
#
# TIP: For reTerminal / S3 devices, if you cannot see logs via USB,
#      add this to your base 'logger:' section:
#      hardware_uart: USB_CDC
#
# ============================================================================

# ====================================
# Device Settings
# ====================================
# Orientation: landscape
# Dark Mode: disabled
# Refresh Interval: 600
# Power Strategy: Ultra Eco (Deep Sleep)
# Deep Sleep Interval: 600
# ====================================

# esphome:
#   name: your-device-name
#   comment: 'Snippet generated by ESPHome Designer'
#   on_boot:
#     priority: 300
#     then:
#       - output.turn_on: bsp_battery_enable
#       - delay: 2s
#       - script.execute: deep_sleep_cycle
#
# esp32:
#   board: esp32-s3-devkitc-1
#   framework:
#     type: esp-idf
#     sdkconfig_options:
#       CONFIG_ESP32S3_DEFAULT_CPU_FREQ_240: y
#       CONFIG_ESP32S3_DATA_CACHE_64KB: y
#
# logger:
#   hardware_uart: USB_CDC # Enable for USB debugging on S3
#   level: DEBUG
#
# api:
# ota:
# wifi:
#   # ... your wifi config here

deep_sleep:
  id: deep_sleep_control
  run_duration: 30s # Stay awake 30s on boot for OTA
  sleep_duration: 600s


globals:
  - id: display_page
    type: int
    restore_value: true
    initial_value: '0'
  - id: page_refresh_default_s
    type: int
    restore_value: true
    initial_value: '600'
  - id: page_refresh_current_s
    type: int
    restore_value: false
    initial_value: '60'
  - id: last_page_switch_time
    type: uint32_t
    restore_value: false
    initial_value: '0'
psram:
  mode: octal
  speed: 80MHz

http_request:
  verify_ssl: false
  timeout: 20s
  buffer_size_rx: 4096
i2c:
  - sda: GPIO17
    scl: GPIO18
    scan: true
    id: bus_a

spi:
  - id: spi_bus
    clk_pin: GPIO7
    mosi_pin: GPIO9

output:
  - platform: gpio
    pin: GPIO6
    id: bsp_battery_enable

time:
  - platform: homeassistant
    id: ha_time
sensor:
  - platform: adc
    pin: GPIO1
    name: "Battery Voltage"
    unit_of_measurement: "V"
    device_class: voltage
    state_class: measurement
    id: battery_voltage
    update_interval: 60s
    attenuation: 12db
    filters:
      - multiply: 2

  - platform: template
    name: "Battery Level"
    id: battery_level
    unit_of_measurement: "%"
    icon: "mdi:battery"
    device_class: battery
    state_class: measurement
    lambda: 'return id(battery_voltage).state;'
    update_interval: 60s
    filters:
      - calibrate_linear:
          - 4.15 -> 100
          - 3.96 -> 90
          - 3.91 -> 80
          - 3.85 -> 70
          - 3.8 -> 60
          - 3.75 -> 50
          - 3.68 -> 40
          - 3.58 -> 30
          - 3.49 -> 20
          - 3.41 -> 10
          - 3.3 -> 5
          - 3.27 -> 0
      - clamp:
          min_value: 0
          max_value: 100

  - platform: homeassistant
    id: weather_high_day0
    entity_id: sensor.weather_forecast_day_0_high
    unit_of_measurement: '°C'
    internal: true
  - platform: homeassistant
    id: weather_low_day0
    entity_id: sensor.weather_forecast_day_0_low
    unit_of_measurement: '°C'
    internal: true
  - platform: homeassistant
    id: weather_high_day1
    entity_id: sensor.weather_forecast_day_1_high
    unit_of_measurement: '°C'
    internal: true
  - platform: homeassistant
    id: weather_low_day1
    entity_id: sensor.weather_forecast_day_1_low
    unit_of_measurement: '°C'
    internal: true
  - platform: homeassistant
    id: weather_high_day2
    entity_id: sensor.weather_forecast_day_2_high
    unit_of_measurement: '°C'
    internal: true
  - platform: homeassistant
    id: weather_low_day2
    entity_id: sensor.weather_forecast_day_2_low
    unit_of_measurement: '°C'
    internal: true
  - platform: homeassistant
    id: weather_high_day3
    entity_id: sensor.weather_forecast_day_3_high
    unit_of_measurement: '°C'
    internal: true
  - platform: homeassistant
    id: weather_low_day3
    entity_id: sensor.weather_forecast_day_3_low
    unit_of_measurement: '°C'
    internal: true
  - platform: homeassistant
    id: weather_high_day4
    entity_id: sensor.weather_forecast_day_4_high
    unit_of_measurement: '°C'
    internal: true
  - platform: homeassistant
    id: weather_low_day4
    entity_id: sensor.weather_forecast_day_4_low
    unit_of_measurement: '°C'
    internal: true
  - platform: wifi_signal
    id: wifi_signal_dbm
    internal: true
  - platform: homeassistant
    id: sensor_greenhouse_temperature
    entity_id: sensor.greenhouse_temperature
    internal: true
  - platform: homeassistant
    id: sensor_outside_temperature
    entity_id: sensor.outside_temperature
    internal: true
  - platform: homeassistant
    id: sensor_kitchen_temp_temperature
    entity_id: sensor.kitchen_temp_temperature
    internal: true
  - platform: homeassistant
    id: sensor_lounge_temp_temperature
    entity_id: sensor.lounge_temp_temperature
    internal: true
  - platform: homeassistant
    id: sensor_0x00158d00044a3bca_temperature
    entity_id: sensor.0x00158d00044a3bca_temperature
    internal: true
  - platform: homeassistant
    id: sensor_bathroom_temperature
    entity_id: sensor.bathroom_temperature
    internal: true
  - platform: homeassistant
    id: sensor_basement_sensor_temperature
    entity_id: sensor.basement_sensor_temperature
    internal: true
  - platform: homeassistant
    id: sensor_greenhouse_humidity
    entity_id: sensor.greenhouse_humidity
    internal: true
  - platform: homeassistant
    id: sensor_outside_humidity
    entity_id: sensor.outside_humidity
    internal: true
  - platform: homeassistant
    id: sensor_kitchen_temp_humidity
    entity_id: sensor.kitchen_temp_humidity
    internal: true
  - platform: homeassistant
    id: sensor_lounge_temp_humidity
    entity_id: sensor.lounge_temp_humidity
    internal: true
  - platform: homeassistant
    id: sensor_0x00158d00044a3bca_humidity
    entity_id: sensor.0x00158d00044a3bca_humidity
    internal: true
  - platform: homeassistant
    id: sensor_bathroom_humidity
    entity_id: sensor.bathroom_humidity
    internal: true
  - platform: homeassistant
    id: sensor_basement_sensor_humidity
    entity_id: sensor.basement_sensor_humidity
    internal: true
  - platform: homeassistant
    id: sensor_tautulli_watching
    entity_id: sensor.tautulli_watching
    internal: true
  - platform: homeassistant
    id: sensor_tautulli_direct_streams
    entity_id: sensor.tautulli_direct_streams
    internal: true
  - platform: homeassistant
    id: sensor_tautulli_direct_plays
    entity_id: sensor.tautulli_direct_plays
    internal: true
  - platform: homeassistant
    id: sensor_tautulli_transcodes
    entity_id: sensor.tautulli_transcodes
    internal: true
text_sensor:

  # Weather Forecast Condition Sensors
  - platform: homeassistant
    id: weather_cond_day0
    entity_id: sensor.weather_forecast_day_0_condition
    internal: true
  - platform: homeassistant
    id: weather_cond_day1
    entity_id: sensor.weather_forecast_day_1_condition
    internal: true
  - platform: homeassistant
    id: weather_cond_day2
    entity_id: sensor.weather_forecast_day_2_condition
    internal: true
  - platform: homeassistant
    id: weather_cond_day3
    entity_id: sensor.weather_forecast_day_3_condition
    internal: true
  - platform: homeassistant
    id: weather_cond_day4
    entity_id: sensor.weather_forecast_day_4_condition
    internal: true

  # ============================================================================
  # HOME ASSISTANT TEMPLATE SENSORS
  # Add these template sensors to your Home Assistant configuration.yaml:
  # ============================================================================
  #
  # template:
  #   - trigger:
  #       - trigger: state
  #         entity_id: weather.forecast_home
  #       - trigger: time_pattern
  #         hours: '/1'
  #     action:
  #       - action: weather.get_forecasts
  #         target:
  #           entity_id: weather.forecast_home
  #         data:
  #           type: daily
  #         response_variable: forecast_data
  #     sensor:
  #       - name: 'Weather Forecast Day 0 High'
  #         unique_id: weather_forecast_day_0_high
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[0].temperature | default("N/A") }}'
  #       - name: 'Weather Forecast Day 0 Low'
  #         unique_id: weather_forecast_day_0_low
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[0].templow | default("N/A") }}'
  #       - name: 'Weather Forecast Day 0 Condition'
  #         unique_id: weather_forecast_day_0_condition
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[0].condition | default("cloudy") }}'
  #       - name: 'Weather Forecast Day 1 High'
  #         unique_id: weather_forecast_day_1_high
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[1].temperature | default("N/A") }}'
  #       - name: 'Weather Forecast Day 1 Low'
  #         unique_id: weather_forecast_day_1_low
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[1].templow | default("N/A") }}'
  #       - name: 'Weather Forecast Day 1 Condition'
  #         unique_id: weather_forecast_day_1_condition
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[1].condition | default("cloudy") }}'
  #       - name: 'Weather Forecast Day 2 High'
  #         unique_id: weather_forecast_day_2_high
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[2].temperature | default("N/A") }}'
  #       - name: 'Weather Forecast Day 2 Low'
  #         unique_id: weather_forecast_day_2_low
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[2].templow | default("N/A") }}'
  #       - name: 'Weather Forecast Day 2 Condition'
  #         unique_id: weather_forecast_day_2_condition
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[2].condition | default("cloudy") }}'
  #       - name: 'Weather Forecast Day 3 High'
  #         unique_id: weather_forecast_day_3_high
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[3].temperature | default("N/A") }}'
  #       - name: 'Weather Forecast Day 3 Low'
  #         unique_id: weather_forecast_day_3_low
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[3].templow | default("N/A") }}'
  #       - name: 'Weather Forecast Day 3 Condition'
  #         unique_id: weather_forecast_day_3_condition
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[3].condition | default("cloudy") }}'
  #       - name: 'Weather Forecast Day 4 High'
  #         unique_id: weather_forecast_day_4_high
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[4].temperature | default("N/A") }}'
  #       - name: 'Weather Forecast Day 4 Low'
  #         unique_id: weather_forecast_day_4_low
  #         unit_of_measurement: '°C'
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[4].templow | default("N/A") }}'
  #       - name: 'Weather Forecast Day 4 Condition'
  #         unique_id: weather_forecast_day_4_condition
  #         state: '{{ forecast_data["weather.forecast_home"].forecast[4].condition | default("cloudy") }}'
  #
  # ============================================================================
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO2
      mode: INPUT_PULLUP
      inverted: true
    name: "Left Button"
    id: button_left
    on_press:
      then:
        - script.execute:
            id: change_page_to
            target_page: !lambda 'return id(display_page) > 0 ? id(display_page) - 1 : 0;'
  - platform: gpio
    pin:
      number: GPIO5
      mode: INPUT_PULLUP
      inverted: true
    name: "Refresh Button"
    id: button_refresh
    on_press:
      then:
        - component.update: epaper_display

button:
  - platform: template
    name: "Next Page"
    on_press:
      then:
        - script.execute:
            id: change_page_to
            target_page: !lambda 'return id(display_page) + 1;'
  - platform: template
    name: "Previous Page"
    on_press:
      then:
        - script.execute:
            id: change_page_to
            target_page: !lambda 'return id(display_page) - 1;'
  - platform: template
    name: "Refresh Display"
    on_press:
      then:
        - component.update: epaper_display
  - platform: template
    name: "Go to Page 1"
    on_press:
      then:
        - script.execute:
            id: change_page_to
            target_page: 0

font:
  - file:
      type: gfonts
      family: "Merriweather"
      weight: 700
      italic: false
    id: font_merriweather_700_20
    size: 20
    glyphsets: [GF_Latin_Kernel]
    ignore_missing_glyphs: true
  - file:
      type: gfonts
      family: "Merriweather"
      weight: 400
      italic: false
    id: font_merriweather_400_14
    size: 14
    glyphsets: [GF_Latin_Kernel]
    ignore_missing_glyphs: true
  - file:
      type: gfonts
      family: "Roboto"
      weight: 400
      italic: false
    id: font_roboto_400_16
    size: 16
    glyphsets: [GF_Latin_Kernel]
    ignore_missing_glyphs: true
  - file:
      type: gfonts
      family: "Merriweather"
      weight: 700
      italic: false
    id: font_merriweather_700_14
    size: 14
    glyphsets: [GF_Latin_Kernel]
    ignore_missing_glyphs: true
  - file:
      type: gfonts
      family: "Merriweather"
      weight: 400
      italic: false
    id: font_merriweather_400_20
    size: 20
    glyphsets: [GF_Latin_Kernel]
    ignore_missing_glyphs: true
  - file:
      type: gfonts
      family: "Merriweather"
      weight: 400
      italic: false
    id: font_merriweather_400_16
    size: 16
    glyphsets: [GF_Latin_Kernel]
    ignore_missing_glyphs: true
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: font_material_design_icons_400_32
    size: 32
    glyphs: ["\U000F0026", "\U000F0590", "\U000F0591", "\U000F0592", "\U000F0593", "\U000F0594", "\U000F0595", "\U000F0596", "\U000F0597", "\U000F0598", "\U000F0599", "\U000F059D", "\U000F059E", "\U000F067E", "\U000F067F"]
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: font_material_design_icons_400_24
    size: 24
    glyphs: ["\U000F0079", "\U000F007B", "\U000F007E", "\U000F0082", "\U000F0083", "\U000F091F", "\U000F0922", "\U000F0925", "\U000F0928", "\U000F092B"]
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: font_material_design_icons_400_40
    size: 40
    glyphs: ["\U000F050F"]
  - file: "fonts/materialdesignicons-webfont.ttf"
    id: font_material_design_icons_400_48
    size: 48
    glyphs: ["\U000F058E", "\U000F06BA"]
script:
  - id: change_page_to
    parameters:
      target_page: int
    then:
      - lambda: |-
          int pages_count = 1;
          int target = target_page;
          while (target < 0) target += pages_count;
          target %= pages_count;

          // Debounce: Ignore page changes within 3000ms of last change
          // (adjusted for e-paper display update time)
          uint32_t now = millis();
          if (now - id(last_page_switch_time) < 3000) {
            ESP_LOGD("display", "Page change ignored (debounce), last switch was %d ms ago", now - id(last_page_switch_time));
            return;
          }

          if (id(display_page) != target) {
            // Set debounce time BEFORE display update (update takes ~1.6s)
            id(last_page_switch_time) = now;
            id(display_page) = target;
            id(epaper_display).update();
            ESP_LOGI("display", "Switched to page %d", target);
            // Restart refresh logic
            if (id(manage_run_and_sleep).is_running()) id(manage_run_and_sleep).stop();
            id(manage_run_and_sleep).execute();
          }

  - id: deep_sleep_cycle
    then:
      - logger.log: "Waiting for sync before Deep Sleep..."
      - wait_until:
          condition:
            lambda: 'return id(ha_time).now().is_valid() && api_is_connected();'
          timeout: 60s
      - delay: 5s
      - component.update: epaper_display
      - delay: 5s # Ensure refresh starts before sleep
      - logger.log: "Entering Deep Sleep now..."
      - deep_sleep.enter: deep_sleep_control

  - id: manage_run_and_sleep
    mode: restart
    then:
      - logger.log: "Waiting for sync..."
      - wait_until:
          condition:
            lambda: 'return id(ha_time).now().is_valid() && api_is_connected();'
          timeout: 60s
      - delay: 5s
      - lambda: |-
          int p = id(display_page);
          int interval = id(page_refresh_default_s);
          bool is_sleep_time = false;
          auto time = id(ha_time).now();
          if (time.is_valid()) {
             int hour = time.hour;
             int start = 0;
             int end = 5;
             if (start < end) {
                 if (hour >= start && hour < end) is_sleep_time = true;
             } else {
                 if (hour >= start || hour < end) is_sleep_time = true;
             }
          }
          if (!is_sleep_time) {
          }
          id(page_refresh_current_s) = interval;
      - component.update: epaper_display
      - delay: !lambda 'return id(page_refresh_current_s) * 1000;'
      - script.execute: manage_run_and_sleep
display:
  - platform: waveshare_epaper
    id: epaper_display
    cs_pin: GPIO44
    dc_pin: GPIO10
    reset_pin: GPIO38
    busy_pin:
      number: GPIO4
      inverted: true
    model: "7.50inv2p"
    rotation: 0
    update_interval: never
    full_update_every: 30

    lambda: |-
      const auto COLOR_WHITE = Color(0, 0, 0); // Inverted for e-ink
      const auto COLOR_BLACK = Color(255, 255, 255); // Inverted for e-ink
      const auto COLOR_RED = Color(255, 0, 0);
      const auto COLOR_GREEN = Color(0, 255, 0);
      const auto COLOR_BLUE = Color(0, 0, 255);
      const auto COLOR_YELLOW = Color(255, 255, 0);
      const auto COLOR_ORANGE = Color(255, 165, 0);
      auto color_off = COLOR_WHITE;
      auto color_on = COLOR_BLACK;

      // Helper to print text with word-wrap at widget boundary
      auto print_wrapped_text = [&](int x, int y, int max_w, int line_h, esphome::font::Font *font, Color color, TextAlign align, const char* text) {
        if (!text || max_w <= 0) return;
        int cx = x;
        int cy = y;
        std::string line;
        std::string word;
        const char* p = text;
        while (*p) {
          // SANITIZATION: Treat newlines, carriage returns, and tabs as spaces for flow
          bool is_space = (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t');
          if (is_space) {
            if (!word.empty()) {
              int ww, wh, wbl, wx;
              font->measure(word.c_str(), &ww, &wx, &wbl, &wh);
              int lw = 0, lx;
              if (!line.empty()) { font->measure(line.c_str(), &lw, &lx, &wbl, &wh); int sw, sx, sbl, sh; font->measure(" ", &sw, &sx, &sbl, &sh); lw += sw; }
              if (lw + ww > max_w && !line.empty()) {
                it.print(cx, cy, font, color, align, line.c_str());
                cy += line_h;
                line = word;
              } else {
                if (!line.empty()) line += " ";
                line += word;
              }
              word.clear();
            }
          } else {
            word += *p;
          }
          p++;
        }
        if (!word.empty()) {
          int ww, wh, wbl, wx;
          font->measure(word.c_str(), &ww, &wx, &wbl, &wh);
          int lw = 0, lx;
          if (!line.empty()) { font->measure(line.c_str(), &lw, &lx, &wbl, &wh); int sw, sx, sbl, sh; font->measure(" ", &sw, &sx, &sbl, &sh); lw += sw; }
          if (lw + ww > max_w && !line.empty()) {
            it.print(cx, cy, font, color, align, line.c_str());
            cy += line_h;
            line = word;
          } else {
            if (!line.empty()) line += " ";
            line += word;
          }
        }
        if (!line.empty()) {
          it.print(cx, cy, font, color, align, line.c_str());
        }
      };

      // Helper to apply a simple grey dither mask for e-paper (checkerboard)
      auto apply_grey_dither_mask = [&](int x_start, int y_start, int w, int h) {
        for (int y = y_start; y < y_start + h; y++) {
          for (int x = x_start; x < x_start + w; x++) {
            if ((x + y) % 2 == 0) it.draw_pixel_at(x, y, COLOR_WHITE);
            else it.draw_pixel_at(x, y, COLOR_BLACK);
          }
        }
      };

      // Helper to apply grey dither to text (subtractive - erases every other black pixel)
      auto apply_grey_dither_to_text = [&](int x_start, int y_start, int w, int h) {
        for (int y = y_start; y < y_start + h; y++) {
          for (int x = x_start; x < x_start + w; x++) {
            if ((x + y) % 2 == 0) it.draw_pixel_at(x, y, COLOR_WHITE);
          }
        }
      };
      int currentPage = id(display_page);
      if (currentPage == 0) {
        // page:name "Overview"
        // page:dark_mode "light"
        // page:refresh_type "interval"
        // page:refresh_time ""
        // Clear screen for this page
        it.fill(COLOR_WHITE);
        color_off = COLOR_WHITE;
        color_on = COLOR_BLACK;
        // widget:line id:w_mlbx8rnxhm16x type:line x:100 y:0 w:3 h:483 stroke:3 color:theme_auto orientation:vertical
        it.filled_rectangle(100, 0, 3, 483, color_on);
        // widget:weather_forecast id:w_mlc06jo94k2vr type:weather_forecast x:0 y:0 w:88 h:480 weather_entity:"weather.forecast_home" layout:vertical show_high_low:true day_font_size:20 temp_font_size:14 icon_size:32 font_family:"Merriweather" color:theme_auto precision:0
        {
          static std::map<std::string, const char*> weather_icons = {
            {"clear-night", "\U000F0594"}, {"cloudy", "\U000F0590"},
            {"exceptional", "\U000F0026"}, {"fog", "\U000F0591"},
            {"hail", "\U000F0592"}, {"lightning", "\U000F0593"},
            {"lightning-rainy", "\U000F067E"}, {"partlycloudy", "\U000F0595"},
            {"pouring", "\U000F0596"}, {"rainy", "\U000F0597"},
            {"snowy", "\U000F0598"}, {"snowy-rainy", "\U000F067F"},
            {"sunny", "\U000F0599"}, {"windy", "\U000F059D"},
            {"windy-variant", "\U000F059E"}
          };
          auto get_icon = [&](const std::string& cond_val) -> const char* {
            return weather_icons.count(cond_val) ? weather_icons[cond_val] : "\U000F0590";
          };
          auto get_day_name = [](int offset) -> std::string {
            if (offset == 0) return "Today";
            auto t = id(ha_time).now();
            if (!t.is_valid()) return "---";
            ESPTime future = ESPTime::from_epoch_local(t.timestamp + (offset * 86400));
            char buf[8]; future.strftime(buf, sizeof(buf), "%a");
            return std::string(buf);
          };
          for (int i = 0; i < 1; i++) {
            it.rectangle(0 + i, 0 + i, 88 - 2 * i, 480 - 2 * i, COLOR_WHITE);
          }
          {
            int dx = 0; int dy = 0 + 3;
            it.printf(dx + 44, dy, id(font_merriweather_700_20), color_on, TextAlign::TOP_CENTER, "%s", get_day_name(0).c_str());
            std::string cond_day = id(weather_cond_day0).state.c_str();
            it.printf(dx + 44, dy + 24, id(font_material_design_icons_400_32), color_on, TextAlign::TOP_CENTER, "%s", get_icon(cond_day));
            float high = id(weather_high_day0).state; float low = id(weather_low_day0).state;
            char temp_buf[32];
            if (std::isnan(high) && std::isnan(low)) {
                sprintf(temp_buf, "--/--");
            } else if (std::isnan(high)) {
                sprintf(temp_buf, "--/%.*f°C", 0, low);
            } else if (std::isnan(low)) {
                sprintf(temp_buf, "%.*f°C/--", 0, high);
            } else {
                sprintf(temp_buf, "%.*f/%.*f°C", 0, high, 0, low);
            }
            it.printf(dx + 44, dy + 60, id(font_merriweather_400_14), color_on, TextAlign::TOP_CENTER, "%s", temp_buf);
          }
          {
            int dx = 0; int dy = 96 + 3;
            it.printf(dx + 44, dy, id(font_merriweather_700_20), color_on, TextAlign::TOP_CENTER, "%s", get_day_name(1).c_str());
            std::string cond_day = id(weather_cond_day1).state.c_str();
            it.printf(dx + 44, dy + 24, id(font_material_design_icons_400_32), color_on, TextAlign::TOP_CENTER, "%s", get_icon(cond_day));
            float high = id(weather_high_day1).state; float low = id(weather_low_day1).state;
            char temp_buf[32];
            if (std::isnan(high) && std::isnan(low)) {
                sprintf(temp_buf, "--/--");
            } else if (std::isnan(high)) {
                sprintf(temp_buf, "--/%.*f°C", 0, low);
            } else if (std::isnan(low)) {
                sprintf(temp_buf, "%.*f°C/--", 0, high);
            } else {
                sprintf(temp_buf, "%.*f/%.*f°C", 0, high, 0, low);
            }
            it.printf(dx + 44, dy + 60, id(font_merriweather_400_14), color_on, TextAlign::TOP_CENTER, "%s", temp_buf);
          }
          {
            int dx = 0; int dy = 192 + 3;
            it.printf(dx + 44, dy, id(font_merriweather_700_20), color_on, TextAlign::TOP_CENTER, "%s", get_day_name(2).c_str());
            std::string cond_day = id(weather_cond_day2).state.c_str();
            it.printf(dx + 44, dy + 24, id(font_material_design_icons_400_32), color_on, TextAlign::TOP_CENTER, "%s", get_icon(cond_day));
            float high = id(weather_high_day2).state; float low = id(weather_low_day2).state;
            char temp_buf[32];
            if (std::isnan(high) && std::isnan(low)) {
                sprintf(temp_buf, "--/--");
            } else if (std::isnan(high)) {
                sprintf(temp_buf, "--/%.*f°C", 0, low);
            } else if (std::isnan(low)) {
                sprintf(temp_buf, "%.*f°C/--", 0, high);
            } else {
                sprintf(temp_buf, "%.*f/%.*f°C", 0, high, 0, low);
            }
            it.printf(dx + 44, dy + 60, id(font_merriweather_400_14), color_on, TextAlign::TOP_CENTER, "%s", temp_buf);
          }
          {
            int dx = 0; int dy = 288 + 3;
            it.printf(dx + 44, dy, id(font_merriweather_700_20), color_on, TextAlign::TOP_CENTER, "%s", get_day_name(3).c_str());
            std::string cond_day = id(weather_cond_day3).state.c_str();
            it.printf(dx + 44, dy + 24, id(font_material_design_icons_400_32), color_on, TextAlign::TOP_CENTER, "%s", get_icon(cond_day));
            float high = id(weather_high_day3).state; float low = id(weather_low_day3).state;
            char temp_buf[32];
            if (std::isnan(high) && std::isnan(low)) {
                sprintf(temp_buf, "--/--");
            } else if (std::isnan(high)) {
                sprintf(temp_buf, "--/%.*f°C", 0, low);
            } else if (std::isnan(low)) {
                sprintf(temp_buf, "%.*f°C/--", 0, high);
            } else {
                sprintf(temp_buf, "%.*f/%.*f°C", 0, high, 0, low);
            }
            it.printf(dx + 44, dy + 60, id(font_merriweather_400_14), color_on, TextAlign::TOP_CENTER, "%s", temp_buf);
          }
          {
            int dx = 0; int dy = 384 + 3;
            it.printf(dx + 44, dy, id(font_merriweather_700_20), color_on, TextAlign::TOP_CENTER, "%s", get_day_name(4).c_str());
            std::string cond_day = id(weather_cond_day4).state.c_str();
            it.printf(dx + 44, dy + 24, id(font_material_design_icons_400_32), color_on, TextAlign::TOP_CENTER, "%s", get_icon(cond_day));
            float high = id(weather_high_day4).state; float low = id(weather_low_day4).state;
            char temp_buf[32];
            if (std::isnan(high) && std::isnan(low)) {
                sprintf(temp_buf, "--/--");
            } else if (std::isnan(high)) {
                sprintf(temp_buf, "--/%.*f°C", 0, low);
            } else if (std::isnan(low)) {
                sprintf(temp_buf, "%.*f°C/--", 0, high);
            } else {
                sprintf(temp_buf, "%.*f/%.*f°C", 0, high, 0, low);
            }
            it.printf(dx + 44, dy + 60, id(font_merriweather_400_14), color_on, TextAlign::TOP_CENTER, "%s", temp_buf);
          }
        }
        // widget:template_sensor_bar id:w_mlc09ve9o96or type:template_sensor_bar x:561 y:440 w:239 h:40 wifi:true temp:false hum:false bat:true bg:true bg_color:white radius:8 border:0 icon_size:24 font_size:16 color:theme_auto wifi_ent:"" temp_ent:"" temp_unit:°C hum_ent:"" bat_ent:""
        {
          auto draw_filled_rrect = [&](int x, int y, int w, int h, int r, auto c) {
            it.filled_rectangle(x + r, y, w - 2 * r, h, c);
            it.filled_rectangle(x, y + r, r, h - 2 * r, c);
            it.filled_rectangle(x + w - r, y + r, r, h - 2 * r, c);
            it.filled_circle(x + r, y + r, r, c);
            it.filled_circle(x + w - r - 1, y + r, r, c);
            it.filled_circle(x + r, y + h - r - 1, r, c);
            it.filled_circle(x + w - r - 1, y + h - r - 1, r, c);
          };
          draw_filled_rrect(561, 440, 239, 40, 8, COLOR_WHITE);
          {
            const char* wifi_icon = "\U000F092B";
            if (id(wifi_signal_dbm).has_state()) {
              float sig = id(wifi_signal_dbm).state;
              if (sig >= -50) wifi_icon = "\U000F0928";
              else if (sig >= -70) wifi_icon = "\U000F0925";
              else if (sig >= -85) wifi_icon = "\U000F0922";
              else wifi_icon = "\U000F091F";
            }
            it.printf(621 - 4, 460, id(font_material_design_icons_400_24), color_on, TextAlign::CENTER_RIGHT, "%s", wifi_icon);
            if (id(wifi_signal_dbm).has_state()) it.printf(621 + 4, 460, id(font_roboto_400_16), color_on, TextAlign::CENTER_LEFT, "%.0fdB", id(wifi_signal_dbm).state);
            else it.printf(621 + 4, 460, id(font_roboto_400_16), color_on, TextAlign::CENTER_LEFT, "--dB");
          }
          {
            const char* bat_icon = "\U000F0082";
            float lvl = id(battery_level).state;
            if (lvl >= 90) bat_icon = "\U000F0079";
            else if (lvl >= 50) bat_icon = "\U000F007E";
            else if (lvl >= 20) bat_icon = "\U000F007B";
            else bat_icon = "\U000F0083";
            it.printf(740 - 4, 460, id(font_material_design_icons_400_24), color_on, TextAlign::CENTER_RIGHT, "%s", bat_icon);
            if (id(battery_level).has_state()) it.printf(740 + 4, 460, id(font_roboto_400_16), color_on, TextAlign::CENTER_LEFT, "%.0f%%", id(battery_level).state);
            else it.printf(740 + 4, 460, id(font_roboto_400_16), color_on, TextAlign::CENTER_LEFT, "--%%");
          }
        }
        // widget:icon id:w_mld3tlt65a681 type:icon x:624 y:0 w:76 h:40 code:F050F size:40 color:theme_auto
        it.printf(662, 20, id(font_material_design_icons_400_40), color_on, TextAlign::CENTER, "%s", "\U000F050F");
        // widget:icon id:w_mld3xgl4eovrk type:icon x:700 y:0 w:100 h:40 code:F058E size:48 color:theme_auto
        it.printf(750, 20, id(font_material_design_icons_400_48), color_on, TextAlign::CENTER, "%s", "\U000F058E");
        // widget:text id:w_mld42ouedtkgm type:text x:400 y:40 w:200 h:40 text:"Greenhouse:"
        it.printf(600, 80, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, "Greenhouse:");
        // widget:text id:w_mld44ixnctkw9 type:text x:400 y:80 w:200 h:40 text:"Outside:"
        it.printf(600, 120, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, "Outside:");
        // widget:text id:w_mld44kyf68kc2 type:text x:400 y:120 w:200 h:40 text:"Kitchen:"
        it.printf(600, 160, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, "Kitchen:");
        // widget:text id:w_mld44mhtfw3k6 type:text x:400 y:160 w:200 h:40 text:"Lounge:"
        it.printf(600, 200, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, "Lounge:");
        // widget:text id:w_mld453mkkxkqk type:text x:400 y:200 w:200 h:40 text:"Study:"
        it.printf(600, 240, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, "Study:");
        // widget:text id:w_mld45bczo741j type:text x:400 y:280 w:200 h:40 text:"Basement:"
        it.printf(600, 320, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, "Basement:");
        // widget:text id:w_mld45bk147i8c type:text x:400 y:240 w:200 h:40 text:"Bathroom:"
        it.printf(600, 280, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, "Bathroom:");
        // widget:sensor_text id:w_mld4ann6l3yec type:sensor_text x:600 y:40 w:100 h:40 entity:"sensor.greenhouse_temperature" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.1f °C", id(sensor_greenhouse_temperature).state);
          print_wrapped_text(700, 80, 100, 24, id(font_merriweather_700_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, wrap_buf);
        }
        // widget:sensor_text id:w_mld4c74g791ft type:sensor_text x:600 y:80 w:100 h:40 entity:"sensor.outside_temperature" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.1f °C", id(sensor_outside_temperature).state);
          print_wrapped_text(700, 120, 100, 24, id(font_merriweather_700_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, wrap_buf);
        }
        // widget:sensor_text id:w_mld4c8iykfrts type:sensor_text x:600 y:120 w:100 h:40 entity:"sensor.kitchen_temp_temperature" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.1f °C", id(sensor_kitchen_temp_temperature).state);
          print_wrapped_text(700, 160, 100, 24, id(font_merriweather_700_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, wrap_buf);
        }
        // widget:sensor_text id:w_mld4cakoy0cbk type:sensor_text x:600 y:160 w:100 h:40 entity:"sensor.lounge_temp_temperature" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.1f °C", id(sensor_lounge_temp_temperature).state);
          print_wrapped_text(700, 200, 100, 24, id(font_merriweather_700_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, wrap_buf);
        }
        // widget:sensor_text id:w_mld4ccet3m3pq type:sensor_text x:600 y:200 w:100 h:40 entity:"sensor.0x00158d00044a3bca_temperature" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.1f °C", id(sensor_0x00158d00044a3bca_temperature).state);
          print_wrapped_text(700, 240, 100, 24, id(font_merriweather_700_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, wrap_buf);
        }
        // widget:sensor_text id:w_mld4ce9zdqxas type:sensor_text x:600 y:240 w:100 h:40 entity:"sensor.bathroom_temperature" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.1f °C", id(sensor_bathroom_temperature).state);
          print_wrapped_text(700, 280, 100, 24, id(font_merriweather_700_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, wrap_buf);
        }
        // widget:sensor_text id:w_mld4cgcwvt5pa type:sensor_text x:600 y:280 w:100 h:40 entity:"sensor.basement_sensor_temperature" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.1f °C", id(sensor_basement_sensor_temperature).state);
          print_wrapped_text(700, 320, 100, 24, id(font_merriweather_700_20), COLOR_BLACK, TextAlign::BOTTOM_RIGHT, wrap_buf);
        }
        // widget:sensor_text id:w_mld4cjzxpxjxo type:sensor_text x:700 y:40 w:100 h:40 entity:"sensor.greenhouse_humidity" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.0f %%", id(sensor_greenhouse_humidity).state);
          print_wrapped_text(750, 80, 100, 24, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_CENTER, wrap_buf);
        }
        // widget:sensor_text id:w_mld4dkrhuqri5 type:sensor_text x:700 y:80 w:100 h:40 entity:"sensor.outside_humidity" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.0f %%", id(sensor_outside_humidity).state);
          print_wrapped_text(750, 120, 100, 24, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_CENTER, wrap_buf);
        }
        // widget:sensor_text id:w_mld4dmhhmkb7l type:sensor_text x:700 y:120 w:100 h:40 entity:"sensor.kitchen_temp_humidity" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.0f %%", id(sensor_kitchen_temp_humidity).state);
          print_wrapped_text(750, 160, 100, 24, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_CENTER, wrap_buf);
        }
        // widget:sensor_text id:w_mld4do3snz0sb type:sensor_text x:700 y:160 w:100 h:40 entity:"sensor.lounge_temp_humidity" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.0f %%", id(sensor_lounge_temp_humidity).state);
          print_wrapped_text(750, 200, 100, 24, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_CENTER, wrap_buf);
        }
        // widget:sensor_text id:w_mld4dpvikcfls type:sensor_text x:700 y:200 w:100 h:40 entity:"sensor.0x00158d00044a3bca_humidity" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.0f %%", id(sensor_0x00158d00044a3bca_humidity).state);
          print_wrapped_text(750, 240, 100, 24, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_CENTER, wrap_buf);
        }
        // widget:sensor_text id:w_mld4drha6hesp type:sensor_text x:700 y:240 w:100 h:40 entity:"sensor.bathroom_humidity" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.0f %%", id(sensor_bathroom_humidity).state);
          print_wrapped_text(750, 280, 100, 24, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_CENTER, wrap_buf);
        }
        // widget:sensor_text id:w_mld4dt52qcfqj type:sensor_text x:700 y:280 w:100 h:40 entity:"sensor.basement_sensor_humidity" format:"value_only"
        {
          char wrap_buf[512];
          sprintf(wrap_buf, "%.0f %%", id(sensor_basement_sensor_humidity).state);
          print_wrapped_text(750, 320, 100, 24, id(font_merriweather_400_20), COLOR_BLACK, TextAlign::BOTTOM_CENTER, wrap_buf);
        }
        // widget:datetime id:w_mld4j5m6nl5in type:datetime x:200 y:440 w:100 h:40 fmt:time_only
        {
          auto now = id(ha_time).now();
          it.strftime(250, 460, id(font_merriweather_700_20), COLOR_BLACK, TextAlign::CENTER, "%H:%M", now);
        }
        // widget:text id:w_mld4jx8m7hsak type:text x:100 y:448 w:100 h:32 text:"Last Update:"
        it.printf(200, 448, id(font_merriweather_400_14), COLOR_BLACK, TextAlign::TOP_RIGHT, "Last Update:");
        // widget:datetime id:w_mld4mzlmaxim7 type:datetime x:300 y:448 w:146 h:32 fmt:weekday_day_month
        {
          auto now = id(ha_time).now();
          it.strftime(373, 464, id(font_merriweather_400_14), COLOR_BLACK, TextAlign::CENTER, "%A %d %B", now);
        }
        // widget:sensor_text id:w_mlemgyyl0bmck type:sensor_text x:664 y:340 w:40 h:40 entity:"sensor.tautulli_watching" format:"value_only_no_unit"
        it.printf(684, 340, id(font_merriweather_700_20), color_on, TextAlign::TOP_CENTER, "%.0f", id(sensor_tautulli_watching).state);
        // widget:sensor_text id:w_mlemijhr7csl8 type:sensor_text x:662 y:380 w:40 h:40 entity:"sensor.tautulli_direct_streams" format:"value_only_no_unit"
        it.printf(682, 420, id(font_merriweather_700_20), color_on, TextAlign::BOTTOM_CENTER, "%.0f", id(sensor_tautulli_direct_streams).state);
        // widget:sensor_text id:w_mlemimeg99bku type:sensor_text x:750 y:379 w:40 h:40 entity:"sensor.tautulli_direct_plays" format:"value_only_no_unit"
        it.printf(770, 419, id(font_merriweather_700_20), color_on, TextAlign::BOTTOM_CENTER, "%.0f", id(sensor_tautulli_direct_plays).state);
        // widget:sensor_text id:w_mlemip10off8n type:sensor_text x:750 y:339 w:40 h:40 entity:"sensor.tautulli_transcodes" format:"value_only_no_unit"
        it.printf(770, 339, id(font_merriweather_700_20), color_on, TextAlign::TOP_CENTER, "%.0f", id(sensor_tautulli_transcodes).state);
        // widget:text id:w_mlemldcnwh916 type:text x:623 y:340 w:40 h:40 text:"Tot"
        it.printf(643, 340, id(font_merriweather_400_20), color_on, TextAlign::TOP_CENTER, "Tot");
        // widget:text id:w_mlemnsloiv1cj type:text x:710 y:339 w:40 h:40 text:"TC"
        it.printf(730, 339, id(font_merriweather_400_20), color_on, TextAlign::TOP_CENTER, "TC");
        // widget:text id:w_mlemp6nkwu3aw type:text x:710 y:379 w:40 h:40 text:"DP"
        it.printf(730, 419, id(font_merriweather_400_20), color_on, TextAlign::BOTTOM_CENTER, "DP");
        // widget:text id:w_mlempmwm7596a type:text x:622 y:379 w:40 h:40 text:"DS"
        it.printf(642, 419, id(font_merriweather_400_20), color_on, TextAlign::BOTTOM_CENTER, "DS");
        // widget:line id:w_mlemjs2hojo95 type:line x:623 y:380 w:161 h:1 stroke:1 color:theme_auto orientation:horizontal
        it.filled_rectangle(623, 380, 161, 1, color_on);
        // widget:line id:w_mlemk41imk319 type:line x:704 y:343 w:1 h:73 stroke:1 color:theme_auto orientation:vertical
        it.filled_rectangle(704, 343, 1, 73, color_on);
        // widget:icon id:w_mlemvyblp0xb1 type:icon x:547 y:350 w:60 h:60 code:F06BA size:48 color:theme_auto
        it.printf(577, 380, id(font_material_design_icons_400_48), color_on, TextAlign::CENTER, "%s", "\U000F06BA");
      }

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions