这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » 活动中心 » 板卡试用 » 【M5PaperESP32E-Ink】HomeAssistant智能消息可视化终

共1条 1/1 1 跳转至

【M5PaperESP32E-Ink】HomeAssistant智能消息可视化终端(成果贴)

菜鸟
2026-04-19 15:49:14     打赏

一、项目介绍

本项目基于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,没法直接用。只能对旧版驱动进行修改重构。

详见【M5PaperESP32E-Ink】ESPHOME适配M5Paper(IT8951E)电子墨水屏驱动-电子产品世界论坛

2、其他模块适配

解决了墨水屏驱动这个最大的难题后,剩下的模块适配就比较顺利了。我对开发板自带的触摸屏(GT911)、RTC 时钟(BM8563)、温湿度传感器(SHT30)这几个核心模块进行了适配,实现一个简单的菜单栏切换显示不同信息。

详见【M5PaperESP32E-Ink】ESPHOME适配GT911触摸驱动-电子产品世界论坛

3、深度睡眠和续航测试

这一步主要实现了两个核心功能:一是把 Home Assistant 里其他设备的传感器数据,同步到 M5Paper 的墨水屏上,实现一站式查看;二是借助 BM8563 RTC 芯片实现深度睡眠,测试设备满电状态下的实际续航和功耗表现。

详见【M5PaperESP32E-Ink】深度睡眠续航实测:1150mAh续航84小时-电子产品世界论坛

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页面:

1776584847935686.jpg

M5paper上墙效果:

1776584861819506.jpg

六、技术难点与心得体会

说实话,整个项目下来,最大的技术难点还是墨水屏的驱动适配。因为 ESPHome 版本更新快,旧版驱动不兼容,只能自己对照官方源码一点点修改、调试,中间踩了不少坑,比如BUSY引脚上电顺序、局部刷新等,折腾了挺久才搞定,这也是整个开发过程中最耗时、最费精力的部分。

再说说心得体会,最大的感悟就是:追求低功耗、长续航,就只能舍弃触摸功能。一开始我还想保留触摸功能,但测试发现,只要开启深度睡眠,整个开发板就相当于切断了电源,触摸也会失效,只有在唤醒的 30 秒内才能短暂使用。最后没办法,只能舍弃触摸功能、保留深度睡眠,算是一个必要的取舍吧。







关键词: M5Paper     ESP32     E-Ink     Assis    

共1条 1/1 1 跳转至

回复

匿名不能发帖!请先 [ 登陆 注册 ]