Works with ESPHome in Home Assistant

Introduction to Home Assistant
Home Assistant is a powerful open-source home automation platform that allows you to control and monitor your smart home devices from a single, unified interface. It acts as the central hub for your smart home, enabling you to automate routines, monitor sensors, and create a more intelligent living space.

Why Home Assistant?
Local Control: Unlike many cloud-based solutions, Home Assistant runs locally on your network, ensuring your data stays private and your automations work even without internet access.
Extensive Device Support: Home Assistant integrates with thousands of different smart home devices and services, making it highly versatile and future-proof.
Powerful Automation: Create sophisticated automation rules that can respond to various triggers like time, device states, sensor readings, and more.
Customizable Dashboard: Design your own user interface to display the information that matters most to you.
Why E-Paper Display with Home Assistant?
The XIAO 7.5" ePaper Panel is an excellent companion for Home Assistant for several reasons:
Energy Efficiency: The e-paper display only consumes power when updating content, making it perfect for displaying persistent information like weather forecasts, calendar events, or system status.
Clear Visibility: Unlike LCD screens, e-paper displays are easily readable in any lighting condition, including direct sunlight, making them ideal for wall-mounted home control panels.
Long Battery Life: Combined with deep sleep mode, the display can operate for months on a single battery charge while still providing valuable information at a glance.
Flexible Integration: Through ESPHome, the display seamlessly integrates with Home Assistant, allowing you to show any data from your smart home system in an elegant, always-visible format.
These advantages make the XIAO 7.5" ePaper Panel an ideal choice for creating an energy-efficient, always-on information display for your Home Assistant setup.
ESPHome Integration
ESPHome is an open-source firmware creation tool specifically designed for ESP8266/ESP32 devices. It allows you to create custom firmware using simple YAML configuration files, which can then be flashed to your device. For the XIAO 7.5" ePaper Panel, ESPHome serves as the essential middleware that enables communication between the device and Home Assistant.
The system works by converting your YAML configuration into fully-featured firmware that runs on your ESP device. This firmware handles all the complex tasks of connecting to your network, communicating with Home Assistant, and controlling the ePaper display. When combined with Home Assistant, ESPHome provides a robust platform for creating sophisticated home automation displays and controls.
Let's explore how to set it up and make the most of this versatile display.
Getting Started
Before the tutorial content of this article begins, you may need to have the following hardware ready.
Materials Required
XIAO 7.5" ePaper Panel | Home Assistant Green |
---|---|
![]() | ![]() |
Home Assistant Green is the easiest and most privacy-focused way to automate your home. It offers an effortless setup and allows you to control all the smart devices with just one system, where all the data is stored locally by default. This board benefits from the thriving Home Assistant ecosystem and it will be improved every month by open source.
We recommend using Home Assistant Green as the Home Assistant host for this tutorial, or you can use any Home Assistant host with a Supervisor.
We have also written how to install Home Assistant for some of Seeed Studio products, please refer to them.
- Getting Started with Home Assistant on ODYSSEY-X86
- Getting Started with Home Assistant on reTerminal
- Getting Started with Home Assistant on LinkStar H68K/reRouter CM4
If you are not using a Seeed Studio product, you can also check and learn how to install Home Assistant for other products on the official Home Assistant website.
Step 1. Install ESPHome
If you have already installed ESPHome, you can skip this step.
Go to Settings -> Add-ons -> ADD-ON STORE


Search ESPHome and click on it. Click on INSTALL and START.
If you can't find ESPHome in the add-on store, make sure you're using a Home Assistant installation that supports add-ons (like Home Assistant OS or supervised installations). For other installation types (like Home Assistant Container), you may need to run the ESPHome Device Builder independently using Docker. See the official ESPHome documentation for more details.

And then, ESPHome Builder will appear at the sidebar.

Step 2. Add a new device
Go to ESPHome and click on NEW DEVICE.

Give the device a name you like and click on NEXT.



After you create a new device, click EDIT.

Step 3. Install firmware
This is a very basic example and will show "Hello World!" on the display.
The main purpose is to show you different ways to install firmware to the device.
After installing ESPHome and adding a new device, you can copy the code below and paste it after captive_portal
as shown below.
Click here to preview the full code
# define font to display words
font:
- file: "gfonts://Inter@700"
id: font1
size: 24
# define SPI interface
spi:
clk_pin: GPIO8
mosi_pin: GPIO10
display:
- platform: waveshare_epaper
cs_pin: GPIO3
dc_pin: GPIO5
busy_pin: GPIO4
reset_pin: GPIO2
model: 7.50inv2
update_interval: 30s
lambda: |-
it.print(0, 0, id(font1), "Hello World!");

Click INSTALL to install the code to the device and you will see the following image.
- Install through browser
- Install through host
- Install through Wi-Fi
If your Home Assistant Host (Raspberry PI/Green/Yellow etc.) is far away from you, we recommend using this method. You can install it with the computer you have on hand.
First, you need to click Manual download to download the compiled firmware.

Open this website where we will upload the firmware to the ePaper panel.

Go back to ESPHome to download the firmware.

Select Factory format.

Use USB cable to connect the ePaper panel to your computer and click CONNECT.

Select usbmodemxxx(Windows is COMxxx) and click connect. Encountered a problem? Click here.

Click INSTALL and select the firmware you just downloaded.

Wait a moment and you will see 'Hello world!' on the display ~


If your Home Assistant Host (Raspberry PI/Green/Yellow etc.) is nearby, we recommend using this method as it is simpler.
Before you install the code to the device, you need to use USB cable to connect this device to your Raspberry Pi or HA Green(Yellow) etc which is running Home Assistant.
Click the options following the image to install the code to the device. Haven't found port when device in deep sleep mode?


Wait a moment and you will see the feedback like the following image. It means the code is running successfully.


This is the simplest way, but on the premise that when installing the program for the first time, you should first upload the program to the ePaper Panel using the method on the left. After that, you can upload it via wifi. Also, make sure your YAML configuration includes properly configured ota
and api
sections with valid encryption keys for this method to work.
In this way, you don't need to connect the ePaper panel to anything, just make sure it is online.
Click the option and then the firmware will be installed to ePaper penal automatically.

Wait a moment and you will see the feedback like the following image. If it fails, it may be due to a weak signal. Please move the device closer to your router. Encountered a problem? Click here.


Basic usages
1. Display shape
This example will show shape on the display.
After installing ESPHome and add a new device, you can copy the code below and paste it to captive_portal part as the following image.
Click here to copy the code.
spi:
clk_pin: GPIO8
mosi_pin: GPIO10
display:
- platform: waveshare_epaper
model: 7.50inv2
cs_pin: GPIO3
dc_pin: GPIO5
reset_pin: GPIO2
busy_pin: GPIO4
update_interval: 5min
lambda: |-
it.rectangle(10, 10, 100, 50);
it.rectangle(150, 10, 50, 50);
it.circle(250, 35, 25);
it.filled_rectangle(10, 80, 100, 50);
it.filled_rectangle(150, 80, 50, 50);
it.filled_circle(250, 105, 25);

When you see the feedback like the following image, it means the code is running successfully.
You can also click here to see more usages.

2. Display information in HA
This example will show the information in HA on the display.
First of all, you need to add this device to HA. Otherwise, you can't get the information from HA.
If HA don't show the device, you should run above demo first. After running above demo, you can see the device in HA.


And then, click SUBMIT and FINISH.


After installing ESPHome and adding a new device, you can copy the code below and paste it after captive_portal
as shown below.
Click here to preview the full code
# Define font to show info
font:
- file: "gfonts://Inter@700"
id: myFont
size: 24
# Get info from HA, as string format
text_sensor:
- platform: homeassistant
entity_id: weather.forecast_home
id: myWeather
internal: true
- platform: homeassistant
entity_id: weather.forecast_home
id: myTemperature
attribute: "temperature"
internal: true
# Get info from HA, as float format
sensor:
- platform: homeassistant
entity_id: weather.forecast_home
id: myPressure
attribute: "pressure"
internal: true
# Display info via SPI
spi:
clk_pin: GPIO8
mosi_pin: GPIO10
display:
- platform: waveshare_epaper
cs_pin: GPIO3
dc_pin: GPIO5
busy_pin: GPIO4
reset_pin: GPIO2
model: 7.50inv2
update_interval: 30s
lambda: |-
//print info in log
ESP_LOGD("epaper", "weather: %s", id(myWeather).state.c_str());
ESP_LOGD("epaper", "temperature: %s", id(myTemperature).state.c_str());
ESP_LOGD("epaper", "pressure: %.1f", id(myPressure).state);
//display info in epaper screen
it.printf(100, 100, id(myFont), "%s", id(myWeather).state.c_str());
it.printf(100, 150, id(myFont), "%s", id(myTemperature).state.c_str());
it.printf(100, 200, id(myFont), "%.1f", id(myPressure).state);
Install those codes to your device.

The function of the code is to get weather, temperature and pressure from HA and display them on the display.

When you see the feedback like the following image, it means the code is running successfully.


3. Display icon
This example will show icon on the display.
First, we need to install a File Editor add-on. Search Studio Code Server and click on it. Click on INSTALL and START.


And then, create a new folder call fonts and download this file and put it into fonts folder.

After installing ESPHome and adding a new device, you can copy the code below and paste it after captive_portal
as shown below.
Click here to preview the full code
font:
- file: 'fonts/materialdesignicons-webfont.ttf' #here is the directory to save ttf file
id: font_mdi_large
size: 200 # big size icon
glyphs: &mdi-weather-glyphs
- "\U000F0595" # weather cloudy
- "\U000F0592" # weather hail
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font_mdi_medium # small size icon
size: 40
glyphs: *mdi-weather-glyphs
spi:
clk_pin: GPIO8
mosi_pin: GPIO10
display:
- platform: waveshare_epaper
cs_pin: GPIO3
dc_pin: GPIO5
busy_pin: GPIO4
reset_pin: GPIO2
model: 7.50inv2
update_interval: 30s
lambda: |-
it.printf(100, 200, id(font_mdi_medium), TextAlign::CENTER, "\U000F0595");
it.printf(400, 200, id(font_mdi_large), TextAlign::CENTER, "\U000F0592");

When you see the feedback like the following image, it means the code is running successfully.

If you want to use other icons, you can click the below button to explore more.
Select a icon you want.

Copy the code and paste it to captive_portal part as the following image.


4. Display image
This example will show any images you like on the display.
Like the previous example, we need to install Studio Code Server and create a new folder call image to save the image.
And then put a image into image folder. You can click the below button to download an image to have a try.

After installing ESPHome and adding a new device, you can copy the code below and paste it after captive_portal
as shown below.
Click here to preview the full code
image:
- file: /config/esphome/image/wifi.jpg # the path where you save the image, png or jpg format
id: myImage
type: BINARY
resize: 800x480 # how big you want to show, the biggest size should be as same as ePaper Penal pixel(800x480)
invert_alpha: true # invert color
spi:
clk_pin: GPIO8
mosi_pin: GPIO10
display:
- platform: waveshare_epaper
cs_pin: GPIO3
dc_pin: GPIO5
busy_pin: GPIO4
reset_pin: GPIO2
model: 7.50inv2
update_interval: 30s
lambda: |-
it.image(0, 0, id(myImage));

When you see the feedback like the following image, it means the code is running successfully.

Demo 1. Take the Home Assistant dashboard as a screenshot
This example will show the screenshot of HA on the display.
First, you need to install an screenshot Add-on Puppet, click here to install.

Please note that the version should be higher than or equal to 1.11.4. After installation, go to Configuration page. We need to create a access_token for this add-on.

See next step to create a token and paste here.

Go to the bottom of Security page and create a token, and then copy and paste it to Puppet add-on.

Remember to restart the Puppet add-on.

Starting the add-on will launch a new server on port 10000. Any path you request will return a screenshot of that page. You will need to specify the viewport size you want.
For example, to get a 1000px x 1000px screenshot of your default dashboard, fetch:
# http://192.168.1.191:10000/lovelace/0?viewport=1000x1000(My address)
http://homeassistant.local:10000/lovelace/0?viewport=1000x1000
To reduce the color palette for E Ink® displays, you can add the eink parameter. The value represents the number of colors (including black) to use. For example, for a 2-color E Ink® display:
http://homeassistant.local:10000/lovelace/0?viewport=1000x1000&eink=2
If you are using eink=2, you can also invert the colors by adding the invert parameter:
http://homeassistant.local:10000/lovelace/0?viewport=1000x1000&eink=2&invert
Besides, you can also screenshot other page, for example To-do lists page in HA:
http://192.168.1.191:10000/todo?viewport=800x480&eink=2&invert
You can take a look the effect of the screenshot by input this link in your browser.

After installing ESPHome and adding a new device, you can copy the code below and paste it after captive_portal
as shown below.
Click here to preview the full code
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.191:10000/todo?viewport=800x480&eink=2&invert #change this link to your screenshot link
update_interval: 30s
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: GPIO4
reset_pin: GPIO2
model: 7.50inv2
update_interval: never
lambda: |-
it.image(0, 0, id(dashboard_image));

When you see the feedback like the following image, it means the code is running successfully.

Demo2. Deep sleep mode
During deep sleep mode, you can't upload code to the device directly. You need to enter the download mode.Click here jump to Q3.
This example will show how to use deep sleep mode to save power. Update info every 6 hours. A 2000mAh battery can last about 3 months.
After installing ESPHome and adding a new device, you can copy the code below and paste it after captive_portal
as shown below.
Click here to preview the full code
globals:
- id: sleep_counter
type: int
restore_value: yes # key parameter, to use RTC storage
initial_value: '0'
# Here is deep sleep part
deep_sleep:
id: deep_sleep_1
run_duration: 30s # Device wake up and run 30s (enough to display)
sleep_duration: 3min # deep sleep for 3min
interval:
- interval: 29s # run this command before the end of run_duration
then:
- logger.log: "Entering deep sleep now..."
font:
- file: "gfonts://Inter@700"
id: font1
size: 24
spi:
clk_pin: GPIO8
mosi_pin: GPIO10
display:
- platform: waveshare_epaper
cs_pin: GPIO3
dc_pin: GPIO5
busy_pin: GPIO4
reset_pin: GPIO2
model: 7.50inv2
update_interval: 3min
lambda: |-
id(sleep_counter) += 1;
ESP_LOGD("main", "Wakeup count: %d", id(sleep_counter));
it.printf(100, 100, id(font1), "Wakeup count: %d", id(sleep_counter));

You'll see a counter. It will increment by one every time it wakes up.

Demo 3. Comprehensive example
For you to understand better, we strongly recommend that you run the basic usages above first.
This example will show how to get weather information and calendar information from HA and display them on the display. What's more, it will use deep sleep mode to save power. Update info every 6 hours. A 2000mAh battery can last about 3 months.
First, you need to check if you have weather component in HA. Normally, you will have one when you install HA.

Also you can go to Developer Tools -> STATES to check if you have weather information in HA. Here is the information you will get later.

Second, you need to install calendar component in HA.
Go to Settings -> Devices & Services -> Integrations -> Add Integration


Select Local Calendar and click SUBMIT button.


After that, you will see the Local Calendar in Configured part and in your sidebar.

Click Calendar in your sidebar and create 3 new calendars name calender, epaper_event and new_calendar. You can also use other name but please keep the same name in your code later.


Before copy the code, please put wifi.jpg, icon ttf file and font ttf file into image folder and fonts folder.
Click here to preview the full code
esphome:
name: dashboard
friendly_name: dashboard
esp32:
board: esp32-c3-devkitm-1
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: "jBgx0v+Y9eKiQmYTk0SCnHgtDowNDZqgFU26Z2VTYzM="
ota:
- platform: esphome
password: "9f78b53ef216c5d689f7408bb1ebe728"
# -------------------------------------- Keep your code above, change your code below --------------------------------------
globals:
- id: wifi_status
type: int
restore_value: no
initial_value: "0"
- id: first_update_done
type: bool
restore_value: no
initial_value: "false"
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;
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: 60min # deep sleep for 1h
script:
- id: update_display
then:
- component.update: my_display
interval:
# Condition: wifi connected && data retrieved && first time
- interval: 10s # Check every second
then:
- if:
condition:
and:
- wifi.connected:
- lambda: "return !id(ha_calendar_event_1).state.empty();"
- lambda: "return !id(first_update_done);"
then:
- lambda: |-
ESP_LOGD("Display", "Updating Display...");
- script.execute: update_display # Refresh immediately
- lambda: "id(first_update_done) = true;"
- interval: 59s # run this command before 1s of run_duration end
then:
- logger.log: "Entering deep sleep now..."
image:
- file: image/wifi.jpg
type: BINARY
id: esphome_logo
resize: 400x240
invert_alpha: true
# Connect to Home Assistant to get time
time:
- platform: homeassistant
id: homeassistant_time
text_sensor:
- platform: homeassistant
id: ha_calendar_event_1
entity_id: calendar.calender
attribute: "message"
- platform: homeassistant
id: ha_calendar_start_time_1
entity_id: calendar.calender
attribute: "start_time"
- platform: homeassistant
id: ha_calendar_end_time_1
entity_id: calendar.calender
attribute: "end_time"
- platform: homeassistant
id: ha_calendar_event_2
entity_id: calendar.epaper_event
attribute: "message"
- platform: homeassistant
id: ha_calendar_start_time_2
entity_id: calendar.epaper_event
attribute: "start_time"
- platform: homeassistant
id: ha_calendar_end_time_2
entity_id: calendar.epaper_event
attribute: "end_time"
- platform: homeassistant
id: ha_calendar_event_3
entity_id: calendar.new_calender
attribute: "message"
- platform: homeassistant
id: ha_calendar_start_time_3
entity_id: calendar.new_calender
attribute: "start_time"
- platform: homeassistant
id: ha_calendar_end_time_3
entity_id: calendar.new_calender
attribute: "end_time"
- platform: homeassistant
entity_id: weather.forecast_home
id: myWeather
- platform: homeassistant
entity_id: weather.forecast_home
id: temp
attribute: "temperature"
- platform: homeassistant
entity_id: weather.forecast_home
id: humi
attribute: "humidity"
- platform: homeassistant
entity_id: weather.forecast_home
id: press
attribute: "pressure"
- platform: homeassistant
entity_id: weather.forecast_home
id: wind
attribute: "wind_speed"
font:
- file: "fonts/Montserrat-Black.ttf"
id: web_font
size: 20
- file: "fonts/Montserrat-Black.ttf"
id: data_font
size: 30
- file: "fonts/Montserrat-Black.ttf"
id: sensor_font
size: 22
- file: "gfonts://Inter@700" #
id: font1
size: 24
- file: 'fonts/materialdesignicons-webfont.ttf' # Directory to save ttf file
id: font_mdi_large
size: 200
glyphs: &mdi-weather-glyphs # https://pictogrammers.com/library/mdi/
- "\U000F050F" # Thermometer
- "\U000F058E" # Humidity
- "\U000F059D" # Wind speed
- "\U000F0D60" # Atmospheric pressure
- "\U000F0590" # Cloudy weather
- "\U000F0596" # Rainy weather
- "\U000F0598" # Snowy weather
- "\U000F0599" # Sunny weather
- file: 'fonts/materialdesignicons-webfont.ttf'
id: font_weather # Copy the above icon and change the size to 40
size: 200
glyphs: *mdi-weather-glyphs
- file: 'fonts/materialdesignicons-webfont.ttf'
id: img_font_sensor # Copy the above icon and change the size to 40
size: 70
glyphs: *mdi-weather-glyphs
spi:
clk_pin: GPIO8
mosi_pin: GPIO10
display:
- platform: waveshare_epaper
id: my_display
cs_pin: GPIO3
dc_pin: GPIO5
busy_pin: GPIO4
reset_pin: GPIO2
model: 7.50inv2
update_interval: 50s
lambda: |-
if(id(wifi_status) == 0){
it.image(180, 0, id(esphome_logo));
it.print(230, 300, id(data_font), "WI-FI CONNECTING");
}else{
// Draw weather images here
std::string weather_string = id(myWeather).state.c_str();
if(weather_string == "rainy" || weather_string == "lightning" || weather_string == "pouring"){
// Draw rainy weather image
it.printf(120, 85, id(font_weather), TextAlign::CENTER, "\U000F0596");
}else if(weather_string == "snowy"){
// Draw snowy weather image
it.printf(120, 85, id(font_weather), TextAlign::CENTER, "\U000F0598");
}else if(weather_string == "sunny" || weather_string == "windy"){
// Draw sunny weather image
it.printf(120, 85, id(font_weather), TextAlign::CENTER, "\U000F0599");
}else{
// Draw cloudy weather image
it.printf(120, 85, id(font_weather), TextAlign::CENTER, "\U000F0590");
}
auto time_now = id(homeassistant_time).now();
// Month conversion
const char* months[] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
const char* month_str = months[time_now.month - 1]; // Month index starts from 0
// Get the day
int day = time_now.day_of_month;
// Draw the date
it.printf(250, 110, id(data_font), "%s %d", month_str, day);
// Get the day of the week
const char* days[] = {"Saturday", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday"};
const char* day_of_week = days[time_now.day_of_week];
it.printf(250, 70, id(data_font), "%s", day_of_week);
int x = 20, y = 180, w = 180, h = 120, r = 10, thickness = 4;
// Draw four borders
it.filled_rectangle(x + r, y, w - 2 * r, thickness); // Top border
it.filled_rectangle(x + r, y + h - thickness, w - 2 * r, thickness); // Bottom border
it.filled_rectangle(x, y + r, thickness, h - 2 * r); // Left border
it.filled_rectangle(x + w - thickness, y + r, thickness, h - 2 * r); // Right border
// Draw four rounded corners
it.filled_circle(x + r, y + r, r); // Top-left corner
it.filled_circle(x + w - r, y + r, r); // Top-right corner
it.filled_circle(x + r, y + h - r, r); // Bottom-left corner
it.filled_circle(x + w - r, y + h - r, r); // Bottom-right corner
// Fill the inside with black to form a border
it.filled_rectangle(x + thickness, y + thickness, w - 2 * thickness, h - 2 * thickness, COLOR_OFF);
// Temperature
it.printf(x+10, y+10, id(sensor_font), "Temperature");
it.printf(x+45, y+75, id(img_font_sensor), TextAlign::CENTER, "\U000F050F");
// Get temperature data
it.printf(x+75,y+65, id(data_font), "%s°F", id(temp).state.c_str());
x = 220;
y = 180;
// Draw four borders
it.filled_rectangle(x + r, y, w - 2 * r, thickness); // Top border
it.filled_rectangle(x + r, y + h - thickness, w - 2 * r, thickness); // Bottom border
it.filled_rectangle(x, y + r, thickness, h - 2 * r); // Left border
it.filled_rectangle(x + w - thickness, y + r, thickness, h - 2 * r); // Right border
// Draw four rounded corners
it.filled_circle(x + r, y + r, r); // Top-left corner
it.filled_circle(x + w - r, y + r, r); // Top-right corner
it.filled_circle(x + r, y + h - r, r); // Bottom-left corner
it.filled_circle(x + w - r, y + h - r, r); // Bottom-right corner
// Fill the inside with black to form a border
it.filled_rectangle(x + thickness, y + thickness, w - 2 * thickness, h - 2 * thickness, COLOR_OFF);
// Humidity
it.printf(x+10, y+10, id(sensor_font), "Humidity");
it.printf(x+45, y+75, id(img_font_sensor), TextAlign::CENTER, "\U000F058E");
// Get humidity data
it.printf(x+75,y+65, id(data_font), "%s%%", id(humi).state.c_str());
x = 20;
y = 320;
// Draw four borders
it.filled_rectangle(x + r, y, w - 2 * r, thickness); // Top border
it.filled_rectangle(x + r, y + h - thickness, w - 2 * r, thickness); // Bottom border
it.filled_rectangle(x, y + r, thickness, h - 2 * r); // Left border
it.filled_rectangle(x + w - thickness, y + r, thickness, h - 2 * r); // Right border
// Draw four rounded corners
it.filled_circle(x + r, y + r, r); // Top-left corner
it.filled_circle(x + w - r, y + r, r); // Top-right corner
it.filled_circle(x + r, y + h - r, r); // Bottom-left corner
it.filled_circle(x + w - r, y + h - r, r); // Bottom-right corner
// Fill the inside with black to form a border
it.filled_rectangle(x + thickness, y + thickness, w - 2 * thickness, h - 2 * thickness, COLOR_OFF);
// Air Pressure
it.printf(x+10, y+10, id(sensor_font), "Air Pressure");
it.printf(x+45, y+75, id(img_font_sensor), TextAlign::CENTER, "\U000F0D60");
// Get atmospheric pressure data
it.printf(x+85,y+50, id(data_font), "%s", id(press).state.c_str());
it.printf(x+85,y+78, id(sensor_font), "inHg");
x = 220;
y = 320;
// Draw four borders
it.filled_rectangle(x + r, y, w - 2 * r, thickness); // Top border
it.filled_rectangle(x + r, y + h - thickness, w - 2 * r, thickness); // Bottom border
it.filled_rectangle(x, y + r, thickness, h - 2 * r); // Left border
it.filled_rectangle(x + w - thickness, y + r, thickness, h - 2 * r); // Right border
// Draw four rounded corners
it.filled_circle(x + r, y + r, r); // Top-left corner
it.filled_circle(x + w - r, y + r, r); // Top-right corner
it.filled_circle(x + r, y + h - r, r); // Bottom-left corner
it.filled_circle(x + w - r, y + h - r, r); // Bottom-right corner
// Fill the inside with black to form a border
it.filled_rectangle(x + thickness, y + thickness, w - 2 * thickness, h - 2 * thickness, COLOR_OFF);
// Wind Speed
it.printf(x+10, y+10, id(sensor_font), "Wind Speed");
it.printf(x+45, y+75, id(img_font_sensor), TextAlign::CENTER, "\U000F059D");
// Get wind speed data
it.printf(x+85,y+50, id(data_font), "%s", id(wind).state.c_str());
it.printf(x+85,y+78, id(sensor_font), "mph");
// Draw a vertical line
it.filled_rectangle(430, 30, 5, 430);
// Right section
it.printf(540, 40, id(data_font), "Calendar");
// Define event structure
struct Event {
std::string message;
std::string start_time;
std::string end_time;
time_t start_timestamp;
};
// Parse time string to time_t (UNIX timestamp)
auto parse_time = [](const std::string &time_str) -> time_t {
struct tm timeinfo = {};
if (strptime(time_str.c_str(), "%Y-%m-%d %H:%M:%S", &timeinfo) == nullptr) {
return 0; // Invalid time
}
return mktime(&timeinfo);
};
// Create event list
std::vector<Event> events = {
{id(ha_calendar_event_1).state, id(ha_calendar_start_time_1).state, id(ha_calendar_end_time_1).state, parse_time(id(ha_calendar_start_time_1).state)},
{id(ha_calendar_event_2).state, id(ha_calendar_start_time_2).state, id(ha_calendar_end_time_2).state, parse_time(id(ha_calendar_start_time_2).state)},
{id(ha_calendar_event_3).state, id(ha_calendar_start_time_3).state, id(ha_calendar_end_time_3).state, parse_time(id(ha_calendar_start_time_3).state)}
};
ESP_LOGD("myCalendar", "Start Time: %s -> %ld", id(ha_calendar_start_time_1).state.c_str(), parse_time(id(ha_calendar_start_time_1).state));
ESP_LOGD("myCalendar", "Start Time: %s -> %ld", id(ha_calendar_start_time_2).state.c_str(), parse_time(id(ha_calendar_start_time_2).state));
ESP_LOGD("myCalendar", "Start Time: %s -> %ld", id(ha_calendar_start_time_3).state.c_str(), parse_time(id(ha_calendar_start_time_3).state));
// Filter invalid events (start_timestamp == 0)
events.erase(std::remove_if(events.begin(), events.end(), [](const Event &e) { return e.start_timestamp == 0; }), events.end());
// Sort by `start_timestamp` (earliest to latest)
std::sort(events.begin(), events.end(), [](const Event &a, const Event &b) {
return a.start_timestamp < b.start_timestamp;
});
// Define a function to format time
auto format_time = [](std::string time_str) -> std::string {
struct tm timeinfo;
if (strptime(time_str.c_str(), "%Y-%m-%d %H:%M:%S", &timeinfo) == nullptr) {
return "Invalid";
}
char buffer[10];
strftime(buffer, sizeof(buffer), "%I:%M%p", &timeinfo); // Convert to 12-hour format
return std::string(buffer);
};
// Parse date
auto format_date = [](const std::string &time_str) -> std::string {
struct tm timeinfo = {};
if (strptime(time_str.c_str(), "%Y-%m-%d %H:%M:%S", &timeinfo) == nullptr) {
return "Invalid";
}
char buffer[6]; // Need to store "MM-DD\0"
strftime(buffer, sizeof(buffer), "%m-%d", &timeinfo);
return std::string(buffer);
};
// Draw events
int even_x_start_offset = 460;
int even_y_start_offset = 80;
for (const auto &event : events) {
if(even_y_start_offset >= 420){
break;
}
// Format time
std::string formatted_date = format_date(event.start_time);
std::string formatted_start_time = format_time(event.start_time);
std::string formatted_end_time = format_time(event.end_time);
// Combine time range string
std::string time_range = formatted_start_time + " - " + formatted_end_time;
time_range = formatted_date + " " + time_range;
if(formatted_start_time == "Invalid" || formatted_end_time == "Invalid"){
time_range.clear();
}
// Display time range, e.g., "10:00AM - 11:00AM"
it.printf(even_x_start_offset, even_y_start_offset, id(sensor_font), "%s", time_range.c_str());
even_y_start_offset += 30;
// Display event name
it.printf(even_x_start_offset, even_y_start_offset, id(sensor_font), "%s", event.message.c_str());
even_y_start_offset += 40;
}
}
When you see the feedback like the following image, it means the code is running successfully.

FAQ
Q1: Why is there no data?

In this case, you should go to Settings -> Devices & Services -> Integrations to RECONGFIGURE device. Haven't found your ePaper Penal? Try to reboot HA.

Q2: Why can't I get those data in Home Assistant?
In this case, you should go to Settings -> Devices & Services -> Integrations to ADD your device to HA.

Q3: How can I upload a new program when device in deep sleep mode?


When device in deep sleep mode, you can't upload a new program derectly.
First, make sure that device is turned on, and then press the Boot button on the back of the board.
Click one time Reset button and release Boot button.
After that, turn off the battery switch and unplug the power cable.
Last, replug the cable and upload a new program.
Q4: How long does the battery last?
Remember to turn on the battery button when charging. Otherwise, the battery won't be able to charge.
After our tests, refresh screen per 6 hours and the battery will last about 3 months in deep sleep mode.
Q5: ePaper Penel can't connect to you computer?

Try unplugging and replugging it several times, or just install the driver according to the prompts.
Q6: Wi-Fi upload program failed?

In this case, you epaper penal is offline or in deep sleep mode. Please get it online or wake it up.
Resources
- [STP]: 3D Model enclosure
- [PDF]: ePaper Driver Board SCH PDF
Tech Support & Product Discussion
Thank you for choosing our products! We are here to provide you with different support to ensure that your experience with our products is as smooth as possible. We offer several communication channels to cater to different preferences and needs.