XIAO 7.5" ePaper Panel - Home Assistant Dashboard

I got a XIAO 7.5" ePaper Panel (early build) while it was on a flash sale from Seeed Studio. This is something you can make yourself, but I took the lazy route.

After playing with it for a little bit, I’ve got a nice battery powered eink display that shows a bunch of Home Assistant entities. I think it looks good and it replaces several of those LCD displays that come with temp probes.

When I first got it I thought I was going to have to build what’s displayed using YAML in ESPHome Builder, but then I read about the Puppet add-on and decided to give that a try instead. Much easier! You just have to create a dashboard in Home Assistant and get it to look right on the 800x480 B&W display. That was the hardest part; getting it to display nicely in B&W. After a bunch of card_mod changes, I got it to look pretty good. I’m still tweaking it, but I’m happy with what I have now.

The big unknown is battery life. They say it’ll last 3 months if you refresh every 6 hours. I’m refreshing every 30 mins, so I’m curious to see how long it last. the one draw back to this product is that there’s currently no way to see the battery status. From some reading, I’m pretty sure I can mod it to so battery status is available, but I’ll hold off on that for now. Hopefully the release version has battery status (it sounds like a fairly easy change).

5 Likes

That looks really cool! I just bought one of these to try out in our new home - any chance you’d be happy to share some of your YAML for the dashboard?

Sure, no problem. You’ll want to use Puppet or something that will snapshot the dashboard and create a image file to display on the ePaper.

Here’s the YAML for ESPHome:

esphome:
  name: kitchendisplay
  friendly_name: KitchenDisplay

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "YourKey"

ota:
  - platform: esphome
    password: "YourPassword"

globals:
  - id: wifi_status
    type: int
    restore_value: no
    initial_value: "0"
  - id: recorded_display_refresh
    type: int
    restore_value: yes
    initial_value: '0'    

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  on_connect:
    then:
      - lambda: |-
          id(wifi_status) = 1;
  on_disconnect:
    then:
      - lambda: |-
          id(wifi_status) = 0;
  ap:
    ssid: "Kitchendisplay Fallback Hotspot"
    password: "YourPassword"


captive_portal:

# Here is deep sleep part
deep_sleep:
  id: deep_sleep_1
  run_duration: 1min  # Device wake up and run 60s (enough to pull data and update)
  sleep_duration: 30min  # deep sleep for 30m

http_request:
  verify_ssl: false
  timeout: 10s
  watchdog_timeout: 15s

online_image:
  - id: dashboard_image
    format: PNG
    type: BINARY
    buffer_size: 30000
    url: http://192.168.1.101:10000/dashboard-robin/display?viewport=800x480&eink=2&invert #change this link to your screenshot link
    update_interval: 20s
    on_download_finished:
      - delay: 0ms
      - component.update: main_display

spi:
  clk_pin: GPIO8
  mosi_pin: GPIO10

display:
  - platform: waveshare_epaper
    id: main_display
    cs_pin: GPIO3
    dc_pin: GPIO5
    busy_pin: 
      number: GPIO4
      inverted: true
    reset_pin: GPIO2
    model: 7.50inv2
    update_interval: never
    lambda: |-
      it.image(0, 0, id(dashboard_image));

time:
  - platform: homeassistant
    id: homeassistant_time
          
sensor:
  - platform: wifi_signal # Reports the WiFi signal strength/RSSI in dB
    name: "WiFi Signal dB"
    id: wifi_signal_db
    update_interval: 120s
    entity_category: "diagnostic"

  - platform: copy # Reports the WiFi signal strength in %
    source_id: wifi_signal_db
    name: "WiFi Signal Percent"
    id: wifi_signal_percent
    filters:
      - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0);
    unit_of_measurement: "Signal %"
    entity_category: "diagnostic"

  - platform: uptime
    name: Uptime

  - platform: internal_temperature
    name: "Internal Temperature"

  - platform: template
    name: "Display Last Update"
    device_class: timestamp
    entity_category: "diagnostic"
    id: display_last_update
    lambda: 'return id(homeassistant_time).now().timestamp;'

  - platform: template
    name: "Display Refresh Count"
    accuracy_decimals: 0
    unit_of_measurement: "Refreshes"
    state_class: "total_increasing"
    entity_category: "diagnostic"
    lambda: 'return id(recorded_display_refresh) += 1;'
    


# Battery read ADC
#  - platform: adc
#    pin: GPIO4
#    name: "A2 Voltage"
#    id: LIION
#    update_interval: never
#    attenuation: 2.5db
#    internal: true
#  - platform: template
#    name: "Battery Voltage"
#    id: bat_v
#    unit_of_measurement: 'V'
#    update_interval: never
#    accuracy_decimals: 2
#    icon: "mdi:battery"
#    lambda: |-
#      return (id(LIION).state * 4);
#  - platform: template
#    name: "Battery Percentage"
#    id: bat_percent
#    unit_of_measurement: '%'
#    update_interval: never
#    accuracy_decimals: 0
#    device_class: battery
#    lambda: |-
#      if(id(bat_v).state < 1)
#      {return 0;}
#      return ((id(bat_v).state-3) /1.2 * 100.00);
  

Here’s the YAML for the dashboard:

type: masonry
path: display
title: Display
cards:
  - type: vertical-stack
    cards:
      - show_current: true
        show_forecast: true
        type: weather-forecast
        entity: weather.kpajamis12
        forecast_type: daily
        secondary_info_attribute: humidity
        card_mod:
          style: |
            :host {
              --weather-icon-clear-night: url("/local/community/weather-card-pics/icons8-moon-and-stars-100.png");
              --weather-icon-cloudy: url("/local/community/weather-card-pics/icons8-cloud-100.png");
              --weather-icon-fog: url("/local/community/weather-card-pics/icons8-fog-100.png");
              --weather-icon-lightning: url("/local/community/weather-card-pics/icons8-cloud-lightning-100.png");
              --weather-icon-lightning-rainy: url("/local/community/weather-card-pics/icons8-stormy-weather-100.png");
              --weather-icon-partlycloudy: url("/local/community/weather-card-pics/icons8-partly-cloudy-day-100.png");
              --weather-icon-pouring: url("/local/community/weather-card-pics/icons8-torrential-rain-100.png");
              --weather-icon-rainy: url("/local/community/weather-card-pics/icons8-rain-100.png");
              --weather-icon-hail: url("/local/community/weather-card-pics/icons8-snow-storm-100.png");
              --weather-icon-snowy: url("/local/community/weather-card-pics/icons8-snow-100.png");
              --weather-icon-snowy-rainy: url("/local/community/weather-card-pics/icons8-snow-100.png");
              --weather-icon-sunny: url("/local/community/weather-card-pics/icons8-sun-100.png");
              --weather-icon-windy: url("/local/community/weather-card-pics/icons8-wind-100.png");
              --weather-icon-windy-variant: url("/local/community/weather-card-pics/icons8-wind-100.png");
              --weather-icon-exceptional: url("/local/community/weather-card-pics/icons8-rainbow-100.png");
            }  
            ha-card {
              border: 2px solid black;
              padding: 10px !important;
            }
            .name-state .name {
              color: black;
              #font-size: 2px;
            }
            .name-state .state {
              color: black;
              #font-size: 7px;
            }
            .temp-attribute .temp {
              color: black;
              font-size: 26px !important;
            }
            .temp-attribute .temp span {
              color: black;
              #font-size: 7px !important;
            }
            .temp-attribute .attribute {
              color: black;
              font-size: 16px;
            }
            .forecast .temp {
              color: black;
              font-size: 20px;
            }
            .forecast .templow {
              color: black;
              font-size: 14px;      
            }
            .forecast div {
              color: black;
            }
            ha-card div.forecast div.temp {
              font-size: 14pt;
            } 
      - type: horizontal-stack
        cards:
          - graph: none
            type: sensor
            detail: 2
            entity: sensor.home_thermostat_air_temperature
            name: Inside
            icon: mdi:home-thermometer
            hours_to_show: 12
            card_mod:
              style: |
                .header .icon {
                  color: black;
                }
                ha-card {
                  border: 2px solid black;
                  --secondary-text-color: black;  
                }         
          - graph: none
            type: sensor
            detail: 2
            name: Outside
            icon: mdi:sun-thermometer-outline
            hours_to_show: 12
            entity: sensor.acurite_5n1_a_3430_temperature
            card_mod:
              style: |
                .header .icon {
                  color: black;
                }  
                ha-card {
                  border: 2px solid black;
                  --secondary-text-color: black;
                }        
          - graph: none
            type: sensor
            detail: 2
            entity: sensor.aqualogic_pool_temperature
            name: Pool
            icon: mdi:pool-thermometer
            hours_to_show: 12
            card_mod:
              style: |
                .header .icon {
                  color: black;

                }
                ha-card {
                  border: 2px solid black;
                  --secondary-text-color: black;  
                }
      - type: horizontal-stack
        cards:
          - graph: none
            type: sensor
            detail: 2
            icon: ""
            hours_to_show: 12
            entity: sensor.acurite_5n1_a_3430_humidity
            name: Humidity
            card_mod:
              style: |
                .header .icon {
                  color: black;
                }
                ha-card {
                  border: 2px solid black;
                  --secondary-text-color: black;  
                }        
          - graph: none
            type: sensor
            detail: 2
            entity: sensor.topwindspeedhr
            name: Wind
            icon: mdi:weather-windy
            hours_to_show: 12
            card_mod:
              style: |
                .header .icon {
                  color: black;
                }
                ha-card {
                  border: 2px solid black;
                  --secondary-text-color: black;  
                }      
          - graph: none
            type: sensor
            detail: 2
            name: Rain
            icon: ""
            hours_to_show: 24
            entity: sensor.kpajamis12_precipitation_today
            card_mod:
              style: |
                .header .icon {
                  color: black;
                }
                ha-card {
                  border: 2px solid black;
                  --secondary-text-color: black;
                }             
  - type: vertical-stack
    cards:
      - entities:
          - entity: calendar.birthdays
            show_time: true
          - entity: calendar.philadelphia_phillies
            show_time: true
          - entity: calendar.carolina_panthers
            show_time: true
          - entity: calendar.holidays_in_united_states
            show_time: true
            split_multiday_events: false
          - entity: calendar.north_carolina_tar_heels_men_s_basketball
            show_time: true
          - entity: calendar.philadelphia_eagles
            show_time: true
          - entity: calendar.email_gmail_com
            show_time: true
        days_to_show: 5
        show_empty_days: true
        filter_duplicates: true
        title_color: ""
        accent_color: Black
        day_spacing: 4px
        event_spacing: 2px
        day_separator_width: 1px
        day_separator_color: var(--primary-text-color)
        today_indicator: dot
        today_indicator_color: var(--primary-text-color)
        event_font_size: 18px
        time_font_size: 14px
        time_color: var(--primary-text-color)
        show_location: false
        weather:
          position: none
          date:
            show_conditions: true
            show_high_temp: true
            show_low_temp: false
            icon_size: 14px
            font_size: 12px
            color: var(--primary-text-color)
          event:
            show_conditions: true
            show_temp: true
            icon_size: 14px
            font_size: 12px
            color: var(--primary-text-color)
          entity: weather.kpajamis12
        type: custom:calendar-card-pro
        height: 350px
        card_mod:
          style: |
            ha-card {
              border: 2px solid black;
            } 
      - type: heading
        icon: mdi:update
        heading: Last Update
        heading_style: subtitle
        badges:
          - type: entity
            show_state: true
            show_icon: false
            entity: sensor.date_time
            color: black
        card_mod:
          style: |
            ha-card .title p { 
              font-size: 14px; 
              padding: 0px !important;
            }   
badges:
  - type: custom:mod-card
    card:
      type: custom:mushroom-template-badge
      content: "{{states('cover.ratgdov25i_1bdf3a_door') | capitalize  }}"
      icon: |
        {% if is_state('cover.ratgdov25i_1bdf3a_door', 'closed') %}
          mdi:garage
        {% else %}
          mdi:garage-open
        {% endif %}
      color: grey
      entity: cover.ratgdov25i_1bdf3a_door
      label: L Garage
      tap_action:
        action: more-info
    card_mod:
      style:
        mushroom-template-badge:
          $: |
            .badge {
              --divider-color: black;
              --ha-card-border-width: 2px;
              --mdc-icon-size: 32px !important;
              .info {
                .label {
                  font-size: 12px;
                  --secondary-text-color: black;
                }
                .content {
                  font-size: 14px;
              }
            }        
        .: |
          ha-card {
            background: none;
            border: none;
          }
  - type: custom:mod-card
    card:
      type: custom:mushroom-template-badge
      content: "{{states('cover.ratgdov25i_1ba3c0_door') | capitalize  }}"
      icon: |
        {% if is_state('cover.ratgdov25i_1ba3c0_door', 'closed') %}
          mdi:garage
        {% else %}
          mdi:garage-open
        {% endif %}
      color: grey
      entity: cover.ratgdov25i_1ba3c0_door
      label: M Garage
      tap_action:
        action: more-info
    card_mod:
      style:
        mushroom-template-badge:
          $: |
            .badge {
              --divider-color: black;
              --ha-card-border-width: 2px;
              --mdc-icon-size: 32px !important;
              .info {
                .label {
                  font-size: 12px;
                  --secondary-text-color: black;
                }
                .content {
                  font-size: 14px;
              }
            }        
        .: |
          ha-card {
            background: none;
            border: none;
          }
  - type: custom:mod-card
    card:
      type: custom:mushroom-template-badge
      content: "{{states('cover.ratgdov25i_1ba3c8_door') | capitalize  }}"
      icon: |
        {% if is_state('cover.ratgdov25i_1ba3c8_door', 'closed') %}
          mdi:garage
        {% else %}
          mdi:garage-open
        {% endif %}
      color: grey
      entity: cover.ratgdov25i_1ba3c8_door
      label: R Garage
      tap_action:
        action: more-info
    card_mod:
      style:
        mushroom-template-badge:
          $: |
            .badge {
              --divider-color: black;
              --ha-card-border-width: 2px;
              --mdc-icon-size: 32px !important;
              .info {
                .label {
                  font-size: 12px;
                  --secondary-text-color: black;
                }
                .content {
                  font-size: 14px;
              }
            }        
        .: |
          ha-card {
            background: none;
            border: none;
          }
  - type: custom:mod-card
    card:
      type: custom:mushroom-template-badge
      content: "{{states('lock.side_door_lock') | capitalize  }}"
      icon: |
        {% if is_state('lock.side_door_lock', 'locked') %}
          mdi:lock
        {% else %}
            mdi:lock-open
        {% endif %}
      color: grey
      entity: lock.side_door_lock
      label: Garage Lock
      tap_action:
        action: more-info
    card_mod:
      style:
        mushroom-template-badge:
          $: |
            .badge {
              --divider-color: black;
              --ha-card-border-width: 2px;
              --mdc-icon-size: 24px !important;
              .info {
                .label {
                  font-size: 12px;
                  --secondary-text-color: black;
                }
                .content {
                  font-size: 14px;
              }
            }        
        .: |
          ha-card {
            background: none;
            border: none;
          }
  - type: custom:mod-card
    card:
      type: custom:mushroom-template-badge
      content: "{{states('input_select.washing_machine_status') }}"
      icon: |
        {% if is_state('input_select.washing_machine_status', 'Off') %}
            mdi:washing-machine-off
        {% elif is_state('input_select.washing_machine_status', 'Not Empty') %}
            mdi:washing-machine-alert
        {% else %}
            mdi:washing-machine
        {% endif %}
      color: grey
      entity: input_select.washing_machine_status
      label: Washer
      tap_action:
        action: more-info
    card_mod:
      style:
        mushroom-template-badge:
          $: |
            .badge {
              --divider-color: black;
              --ha-card-border-width: 2px;
              --mdc-icon-size: 24px !important;
              .info {
                .label {
                  font-size: 12px;
                  --secondary-text-color: black;
                }
                .content {
                  font-size: 14px;
              }
            }        
        .: |
          ha-card {
            background: none;
            border: none;
          }
  - type: custom:mod-card
    card:
      type: custom:mushroom-template-badge
      content: "{{states('input_select.dryer_status') }}"
      icon: |
        {% if is_state('input_select.dryer_status', 'Off') %}
            mdi:tumble-dryer-off
        {% elif is_state('input_select.dryer_status', 'Not Empty') %}
            mdi:tumble-dryer-alert
        {% else %}
            mdi:tumble-dryer
        {% endif %}
      color: grey
      entity: input_select.dryer_status
      label: Dryer
      tap_action:
        action: more-info
    card_mod:
      style:
        mushroom-template-badge:
          $: |
            .badge {
              --divider-color: black;
              --ha-card-border-width: 2px;
              --mdc-icon-size: 24px !important;
              .info {
                .label {
                  font-size: 12px;
                  --secondary-text-color: black;
                }
                .content {
                  font-size: 14px;
              }
            }        
        .: |
          ha-card {
            background: none;
            border: none;
          }

I also changed the icons in the weather card to black and white icons. The default ones work, but sometimes multiple colors blend. I got the icons from icons8.com

2 Likes

+1 to @JGK. Your dashboard is great and packs in a lot of information and thank you for sharing your YAML. What weather forecast integration are you using?

I’m using Wundergroundpws (Weather Underground) that gets data from a AcuRite Iris (5-in-1) that’s in my backyard (KPAJAMIS12). I’ll probably be replacing the AcuRite at some point. It’s worked well for a long time, but the wind speed has never been that actuate.