一、项目介绍
本项目基于M5PAPER开发板,以 ESPHome 为驱动核心,搭配低功耗电子墨水屏硬件,打造一款实用的 Home Assistant 智能消息可视化终端。核心目的就是把 Home Assistant 里有价值的传感器数据、设备状态,通过墨水屏直观地展示出来,不用每次都打开手机APP查看,同时利用墨水屏低功耗的优势,实现长效续航,搞出一套简单、省心、不费电的 Home Assistant 信息展示方案。
二、硬件介绍
本项目使用的硬件是M5Paper ESP32 E-Ink Development Kit V1.1,是M5Stack推出的主打触控墨水屏的开发设备。主控芯片是 ESP32,搭载一块4.7英寸的触摸电子墨水屏,分辨率960×540,整体设计简洁又精致,拿在手里就像一个小号的墨水屏电子书,颜值在线,放在家里当壁挂终端也不突兀,实用性和观赏性都兼顾到了。
相关参数:
SOC:ESP32-D0WDQ6-V3@双核处理器,主频 240MHz
墨水屏:型号:ED047TC1,540 x 960@4.7",灰度 : 16 级
按键:拨轮开关 *1 ,复位按键 *1
RTC:BM8563
温湿度传感器:SHT30
三、设计思路
整体思路很简单,核心就是基于 ESPHome 让 M5Paper 成功接入 Home Assistant,然后通过墨水屏展示 Home Assistant 上的各类数据——包括本地天气、室内各个区域的温湿度、智能设备的功耗、空气质量等传感器信息,还有天气预报这类文本通知消息。
重点利用电子墨水屏“刷新后不耗电”的低功耗优势,再结合开发板自带的 BM8563 RTC 时钟芯片,实现定时唤醒功能:每10分钟唤醒一次设备,同步最新数据、刷新屏幕,之后立刻进入深度睡眠,这样既能保证数据实时性,又能最大限度降低功耗,实际测试续航能达到3-4天,完全满足日常使用需求。
简单梳理一下逻辑流程,非常清晰:
米家等传感器信息
↓
Home Assistant
↓↑
M5PAPER
↓
墨水屏显示
四、实现情况与代码
整个项目都是按照既定计划一步步推进的,没有出现太大的卡顿,主要就是围绕“驱动适配→功能扩展→优化续航→美化界面”这几个步骤来做,下面详细说一下实现过程:
1、墨水屏驱动适配
M5PAPER 基于 ESPHome 接入 Home Assistant,最大的难点其实就是屏幕驱动。因为 ESPHome 一直在持续更新,网上找到的旧版 IT8951E 驱动(墨水屏控制器)已经停更2年了,完全不兼容最新版的 ESPHome,没法直接用。只能对旧版驱动进行修改重构。
详见
2、其他模块适配
解决了墨水屏驱动这个最大的难题后,剩下的模块适配就比较顺利了。我对开发板自带的触摸屏(GT911)、RTC 时钟(BM8563)、温湿度传感器(SHT30)这几个核心模块进行了适配,实现一个简单的菜单栏切换显示不同信息。
详见
3、深度睡眠和续航测试
这一步主要实现了两个核心功能:一是把 Home Assistant 里其他设备的传感器数据,同步到 M5Paper 的墨水屏上,实现一站式查看;二是借助 BM8563 RTC 芯片实现深度睡眠,测试设备满电状态下的实际续航和功耗表现。
详见
4、Home Assistant传感器导入
ESPHome 可通过原生 API 读取 Home Assistant 内的实体状态与属性值并生成本地传感器。基于此,我将 Home Assistant 里常用的、适合在墨水屏上展示的数值类传感器都接了进来,同时也接入了一个文本传感器,用来显示天气预报,这样信息就比较全面了。
① 室外天气:获取厦门天气信息,包括温度、湿度、气压、PM2.5、风速等
# 厦门天气
- platform: homeassistant
name: xiamen temperature
entity_id: sensor.xiamen_temperature
id: xiamen_temp
unit_of_measurement: "°C"
- platform: homeassistant
name: xiamen humidity
id: xiamen_humi
entity_id: sensor.xiamen_humidity
- platform: homeassistant
name: xiamen pressure
id: xiamen_pressure
entity_id: sensor.xiamen_atmospheric_pressure
unit_of_measurement: "hPa"
- platform: homeassistant
name: xiamen pm25
id: xiamen_pm25
entity_id: sensor.xiamen_pm25
- platform: homeassistant
name: xiamen windspeed
id: xiamen_windspeed
entity_id: sensor.xiamen_wind_speed
unit_of_measurement: "km/h"
② 温湿度计:分别接入小米温湿度计与青萍温湿度计的温度、湿度数据
# 小米温湿度计
- platform: homeassistant
name: xiaomi temperature
entity_id: sensor.cleargras_cn_blt_3_u5c2qgggk400_dk1_temperature_p_2_1
id: xiaomi_temp
unit_of_measurement: "°C"
- platform: homeassistant
name: xiaomi humidity
id: xiaomi_humi
entity_id: sensor.cleargras_cn_blt_3_u5c2qgggk400_dk1_relative_humidity_p_2_2
# 青萍温湿度计
- platform: homeassistant
name: qingping temperature
entity_id: sensor.cgllc_cn_blt_3_1mitqg1roc800_dove_temperature_p_2_1
id: qp_temp
unit_of_measurement: "°C"
- platform: homeassistant
name: qingping humidity
id: qp_humi
entity_id: sensor.cgllc_cn_blt_3_1mitqg1roc800_dove_relative_humidity_p_2_2
③ 室内空气质量监测(ZM1):室内 PM2.5 颗粒物浓度与甲醛浓度数据(不考虑准确性,仅做展示用)
# ZM1
- platform: homeassistant
name: zm1 pm25
id: zm1_pm25
entity_id: sensor.zm1_b0f89324aed7_pm25
- platform: homeassistant
name: zm1 hcho
id: zm1_hcho
entity_id: sensor.zm1_b0f89324aed7_hcho
unit_of_measurement: "mg/m3"
④ 群晖NAS设备状态:监控 NAS 的 CPU 占用率、内存使用率、硬盘温度
# 群晖
- platform: homeassistant
name: nas temperature
entity_id: sensor.cnas2010_temperature
id: nas_temp
unit_of_measurement: "°C"
- platform: homeassistant
name: nas cpu
id: nas_cpu
entity_id: sensor.cnas2010_cpu_utilization_total
- platform: homeassistant
name: nas mem
id: nas_mem
entity_id: sensor.cnas2010_memory_usage_real
unit_of_measurement: "%"
- platform: homeassistant
name: disk1 temperature
entity_id: sensor.cnas2010_disk_1_temperature
id: disk1_temp
unit_of_measurement: "°C"
- platform: homeassistant
name: disk2 temperature
entity_id: sensor.cnas2010_disk_2_temperature
id: disk2_temp
unit_of_measurement: "°C"
⑤ 智能排插用电数据:两个智能排插的实时功率与累计用电量,分别监控弱电供电、书桌供电的用电情况
# NAS排插功率
- platform: homeassistant
name: cmpower1 power
entity_id: sensor.cmpower_w1_270cee_24_gong_lu
id: cmpower1_power
unit_of_measurement: "W"
- platform: homeassistant
name: cm1 total power
entity_id: sensor.cmpower_w1_270cee_29_zong_dian_liang
id: cm1_total_power
unit_of_measurement: "Wh"
# 书桌排插功率
- platform: homeassistant
name: cmpower2 power
entity_id: sensor.cmpower_w1_26982d_24_gong_lu
id: cmpower2_power
unit_of_measurement: "W"
- platform: homeassistant
name: cm2 total power
entity_id: sensor.cmpower_w1_26982d_29_zong_dian_liang
id: cm2_total_power
unit_of_measurement: "Wh"
⑥ Home Assistant 文本传感器数据
ESPHome 文本传感器专门用于展示非数值类状态数据,本项目仅接入厦门天气预报,在墨水屏上直观展示天气变化文字描述。
# 文本传感器
text_sensor:
# 厦门天气预报
- platform: homeassistant
id: xiamen_weather
entity_id: sensor.xiamen_forecast_minutely
5、UI视觉优化
为了让墨水屏界面更美观、信息更易读,我们引入 Material Design Icons 图标字体,并对 4.7 寸屏幕进行模块化分区布局,让数据看起来更清晰、更直观,兼顾实用性与视觉效果。
① 图标字体配置
引入 materialdesignicons-webfont.ttf 图标字体,定义大、小两种尺寸的图标,分别用于区域标题和数据标识,用图标替代部分文字,让界面更生动。
# 字体配置
font:
- file: "fonts/materialdesignicons-webfont.ttf"
id: icons_font
size: 36
glyphs:
- "\U000F007A" # 10%
- "\U000F007B" # 20%
- "\U000F007C" # 30%
- "\U000F007D" # 40%
- "\U000F007E" # 50%
- "\U000F007F" # 60%
- "\U000F0080" # 70%
- "\U000F0081" # 80%
- "\U000F0082" # 90%
- "\U000F0079" # 满电量
- "\U000F015F" # 天气
- "\U000F02DC" # 客厅
- "\U000F02E3" # 卧室
- "\U000F011C" # M5PAPER
- file: "fonts/materialdesignicons-webfont.ttf"
id: icons_small
size: 32
glyphs:
- "\U000F00E6" # 广播
- "\U000F050F" # 温度
- "\U000F058E" # 湿度
- "\U000F1A71" # 冰箱温度
- "\U000F140B" # 闪电
- "\U000F08F3" # NAS
- "\U000F0905" # 插座
② 屏幕UI设计
基于 M5Paper 960×540 的竖屏分辨率,采用自上而下、分区展示的布局逻辑,将屏幕划分为五个区域,所有数据对齐排列,用分隔线区分各个区域,避免画面杂乱。
顶部状态栏:电池电量动态图标 + 日期星期 + 大字时间
厦门天气区:气象图标 + 标题,展示温湿度、气压、PM2.5,底部叠加天气预报文本
客厅区域:客厅图标 + 标题,展示小米温湿度、ZM1 空气质量、NAS 排插用电、NAS 设备状态
卧室区域:卧室图标 + 标题,展示青萍温湿度、书桌排插用电数据
本机状态区:M5Paper 图标 + 标题,展示设备自带温湿度、电池电压
同时,还做了数据容错处理:当传感器数据异常(比如断连、读取失败)时,界面不会显示错误内容,避免影响整体观感。
display:
- platform: it8951e
id: m5paper_display
model: M5EPD
cs_pin: GPIO15 # 片选引脚
reset_pin: GPIO23 # 复位引脚
busy_pin: GPIO27 # 忙信号引脚
rotation: 270 # 屏幕旋转角度,0是横向
reversed: False # 不反转颜色
update_interval: never # 不自动刷新,仅手动更新
full_update_every: 10 # 每10次更新执行一次全刷
# 在屏幕上绘制内容
lambda: |-
const int WIDTH = it.get_width(); // 540
const int HEIGHT = it.get_height(); // 960
auto draw_hline = [&](int y) {
it.line(5, y, WIDTH - 5, y);
};
// === 电池图标(右上角)===
float v_bat = id(m5paper_battery_voltage).state;
float bat_pct = NAN;
if (!isnan(v_bat)) {
constexpr float min_v = 3.52f;
constexpr float max_v = 4.15f;
bat_pct = (v_bat - min_v) / (max_v - min_v) * 100.0f;
bat_pct = clamp(bat_pct, 0.0f, 100.0f);
}
const char* bat_icon = "\U000F007A";
if (!isnan(bat_pct)) {
if (bat_pct >= 95) bat_icon = "\U000F0079";
else if (bat_pct >= 85) bat_icon = "\U000F0082";
else if (bat_pct >= 75) bat_icon = "\U000F0081";
else if (bat_pct >= 65) bat_icon = "\U000F0080";
else if (bat_pct >= 55) bat_icon = "\U000F007F";
else if (bat_pct >= 45) bat_icon = "\U000F007E";
else if (bat_pct >= 35) bat_icon = "\U000F007D";
else if (bat_pct >= 25) bat_icon = "\U000F007C";
else if (bat_pct >= 15) bat_icon = "\U000F007B";
else bat_icon = "\U000F007A";
}
it.print(WIDTH - 10, 10, id(icons_font), TextAlign::TOP_RIGHT, bat_icon);
// === 第1行:日期 + 星期 ===
int y = 20;
auto now = id(rtc_time).now();
// 获取星期(0=Sunday, 6=Saturday)
int wday = std::stoi(now.strftime("%w"));
const char* weekdays[] = {"星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"};
char date_week_str[50];
snprintf(date_week_str, sizeof(date_week_str), "%s %s", now.strftime("%Y年%m月%d日").c_str(), weekdays[wday]);
it.print(it.get_width() / 2, y, id(title_font), TextAlign::TOP_CENTER, date_week_str);
y += 50;
// === 第2行:大字时间 ===
it.print(it.get_width() / 2, y, id(big_font), TextAlign::TOP_CENTER, now.strftime("%H:%M").c_str());
y += 80;
// === 天气区 ===
draw_hline(y);
y += 5;
it.print(10, y + 8, id(icons_font), TextAlign::TOP_LEFT, "\U000F015F");
it.print(50, y, id(title_font), TextAlign::TOP_LEFT, "厦门天气");
y += 50;
// 天气数据行1: 温/湿
if (!isnan(id(xiamen_temp).state)) {
it.printf(50, y, id(cn_font), TextAlign::TOP_LEFT, "温度:%.1f℃", id(xiamen_temp).state);
}
if (!isnan(id(xiamen_humi).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "湿度:%.0f%%", id(xiamen_humi).state);
}
y += 45;
// 天气数据行2: 气压/PM2.5
if (!isnan(id(xiamen_pressure).state)) {
it.printf(50, y, id(cn_font), TextAlign::TOP_LEFT, "气压:%.0fhPa", id(xiamen_pressure).state);
}
if (!isnan(id(xiamen_pm25).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "PM2.5:%.0fμg/m³", id(xiamen_pm25).state);
}
y += 45;
// 天气预报文本
if (!id(xiamen_weather).state.empty()) {
it.print(50, y + 8, id(icons_small), TextAlign::TOP_LEFT, "\U000F00E6");
it.print(90, y, id(cn_font), TextAlign::TOP_LEFT, id(xiamen_weather).state.c_str());
y += 45;
}
// === 客厅区 ===
y += 5;
draw_hline(y);
y += 5;
it.print(10, y + 8, id(icons_font), TextAlign::TOP_LEFT, "\U000F02DC");
it.print(50, y, id(title_font), TextAlign::TOP_LEFT, "客厅");
y += 50;
// 小米温湿度
if (!isnan(id(xiaomi_temp).state)) {
it.printf(50, y, id(cn_font), TextAlign::TOP_LEFT, "温度:%.1f℃", id(xiaomi_temp).state);
}
if (!isnan(id(xiaomi_humi).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "湿度:%.1f%%", id(xiaomi_humi).state);
}
y += 45;
// ZM1: PM2.5 + HCHO
if (!isnan(id(zm1_pm25).state)) {
it.printf(50, y, id(cn_font), TextAlign::TOP_LEFT, "PM2.5:%.0fμg/m³", id(zm1_pm25).state);
}
if (!isnan(id(zm1_hcho).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "甲醛:%.2fmg/m³", id(zm1_hcho).state);
}
y += 45;
// 弱电箱: 功率 + 电量
if (!isnan(id(cmpower1_power).state)) {
it.print(50, y + 8, id(icons_small), TextAlign::TOP_LEFT, "\U000F0905");
it.printf(90, y, id(cn_font), TextAlign::TOP_LEFT, "功率:%.0fW", id(cmpower1_power).state);
}
if (!isnan(id(cm1_total_power).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "电量:%.0fkWh", id(cm1_total_power).state / 1000.0f);
}
y += 45;
// NAS: CPU + 内存
if (!isnan(id(nas_cpu).state)) {
it.print(50, y + 8, id(icons_small), TextAlign::TOP_LEFT, "\U000F08F3");
it.printf(90, y, id(cn_font), TextAlign::TOP_LEFT, "CPU:%.0f%%", id(nas_cpu).state);
}
if (!isnan(id(nas_mem).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "内存:%.0f%%", id(nas_mem).state);
}
y += 45;
// 群晖硬盘温度
if (!isnan(id(disk1_temp).state)) {
it.printf(90, y, id(cn_font), TextAlign::TOP_LEFT, "盘1:%.0f℃", id(disk1_temp).state);
}
if (!isnan(id(disk2_temp).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "盘2:%.0f℃", id(disk2_temp).state);
}
y += 45;
// === 卧室区 ===
y += 5;
draw_hline(y);
y += 5;
it.print(10, y + 8, id(icons_font), TextAlign::TOP_LEFT, "\U000F02E3");
it.print(50, y, id(title_font), TextAlign::TOP_LEFT, "卧室");
y += 50;
// 青萍温湿度
if (!isnan(id(qp_temp).state)) {
it.printf(50, y, id(cn_font), TextAlign::TOP_LEFT, "温度:%.1f℃", id(qp_temp).state);
}
if (!isnan(id(qp_humi).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "湿度:%.1f%%", id(qp_humi).state);
}
y += 45;
// 工作台: 功率 + 电量
if (!isnan(id(cmpower2_power).state)) {
it.print(50, y + 8, id(icons_small), TextAlign::TOP_LEFT, "\U000F0905");
it.printf(90, y, id(cn_font), TextAlign::TOP_LEFT, "功率:%.0fW", id(cmpower2_power).state);
}
if (!isnan(id(cm2_total_power).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "电量:%.0fkWh", id(cm2_total_power).state / 1000.0f);
}
y += 45;
// === 本机区 ===
y += 5;
draw_hline(y);
y += 5;
it.print(10, y + 8, id(icons_font), TextAlign::TOP_LEFT, "\U000F011C");
it.print(50, y, id(title_font), TextAlign::TOP_LEFT, "M5Paper");
y += 50;
// 本机温湿度
if (!isnan(id(m5paper_temperature).state)) {
it.printf(50, y, id(cn_font), TextAlign::TOP_LEFT, "温度:%.1f℃", id(m5paper_temperature).state);
}
if (!isnan(id(m5paper_humidity).state)) {
it.printf(290, y, id(cn_font), TextAlign::TOP_LEFT, "湿度:%.1f%%", id(m5paper_humidity).state);
}
y += 45;
// 电池电压
if (!isnan(id(m5paper_battery_voltage).state)) {
it.printf(50, y, id(cn_font), TextAlign::TOP_LEFT, "电压:%.2fV", id(m5paper_battery_voltage).state);
}
五、功能展示
经过多次界面调试与参数优化,解决了数据显示异常、文字或图标显示错乱等问题,最终得到一个比较满意的显示效果,整体简洁、清晰,符合预期。
Home Assistant中M5paper页面:

M5paper上墙效果:

六、技术难点与心得体会
说实话,整个项目下来,最大的技术难点还是墨水屏的驱动适配。因为 ESPHome 版本更新快,旧版驱动不兼容,只能自己对照官方源码一点点修改、调试,中间踩了不少坑,比如BUSY引脚上电顺序、局部刷新等,折腾了挺久才搞定,这也是整个开发过程中最耗时、最费精力的部分。
再说说心得体会,最大的感悟就是:追求低功耗、长续航,就只能舍弃触摸功能。一开始我还想保留触摸功能,但测试发现,只要开启深度睡眠,整个开发板就相当于切断了电源,触摸也会失效,只有在唤醒的 30 秒内才能短暂使用。最后没办法,只能舍弃触摸功能、保留深度睡眠,算是一个必要的取舍吧。
我要赚赏金
