电表折腾续篇:把 ESP32 实时功率与国网电费做进 iOS 锁屏小组件

IMG_5397

早在 2021 年底的时候,我折腾过一篇《在家使用智能电表型号 DDSU666 搭配智能家居芯片 ESP32 详细安装教程》。那时候的思路很纯粹:在入户强电箱里加装一块带 RS485 通讯接口的正泰 DDSU666 单相导轨电表,再通过一块打样的 ESP32-C3 模块刷入 ESPHome 固件,利用 Modbus-RTU 协议把电表的电压、电流和实时功率数据秒级抓取到本地的 Home Assistant 中。

这套硬件在弱电箱里踏踏实实跑了几年,极其稳定,但日常使用中逐渐发现了一个硬伤:本地硬件采集虽然能做到无延迟的秒级功率监控,却无法还原真实的电费账单。国内居民用电存在阶梯电价、分时计费以及各种补贴结算,单靠本地根据脉冲或功率做数值积分推算出来的电费,时间一长必然会和电力公司的官方账单产生偏差。更重要的是,本地电表永远不可能知道电费账户里还剩多少余额。

为了补齐这块拼图,我又在 Home Assistant 中引入了网上国网的集成。两套数据源搭配起来,才算真正覆盖了家庭用电的完整闭环。

不过,数据虽然都进了 Home Assistant,平时的查看体验却一直不算顺手。每次想看一眼家里电器有没有大功率运转,或者查查昨天用了多少度电、电费还剩多少,都得掏出手机、解锁屏幕、点开 HA 的 App、再翻到对应的能源面板卡片。操作链条太长,久而久之就懒得看了。

最近折腾 iOS 锁屏界面时突然意识到:Home Assistant 的 iOS 官方客户端早已支持通过 Jinja2 模板直接自定义锁屏小组件。只要把高频的实时功率和低频的电费统计合理分配到锁屏上,就能实现抬手亮屏即看。

数据源的分工与实体梳理

在动手配置锁屏之前,首先要理清两路数据的分工和对应的实体。

1. 本地硬件源(ESP32-C3 + DDSU666)

负责高频、秒级变动的物理电气指标。通过 ESPHome 的 Modbus 控制器实时轮询,直接走本地局域网推送。

  • 核心实体:sensor.esp32_c3_p(实时有功功率,单位:W)

2. 云端官方源(网上国网集成)

负责低频、具有官方结算效力的财务与统计指标。通常通过 HACS 安装社区主流的 ha-stategrid(网上国网)自定义集成。

集成安装后,在 Home Assistant 的「集成」页面中添加并输入网上国网 App 注册的手机号与验证码完成授权,绑定用电户号。集成会自动根据户号生成一组以 sensor.99_... 开头的长实体 ID。国网的数据是 T+1 结算模式(通常在每天清晨同步前一天的结算数据),每天后台自动轮询拉取几次即可。

在这个集成生成的众多实体中,日常最有价值的是以下三个:

  • 昨日用电量:sensor.99_08eac3dd56454a88a6fbf021d00937cb_02_osg_uc_2608231221_002_3335_previous_daily_usage(单位:kWh / 度)
  • 本月累计用电量:sensor.99_08eac3dd56454a88a6fbf021d00937cb_02_osg_uc_2608231221_002_3335_current_month_usage(单位:kWh / 度)
  • 电费账户可用余额:sensor.99_08eac3dd56454a88a6fbf021d00937cb_02_osg_uc_2608231221_002_3335_account_balance(单位:元)

锁屏小组件的版面规划

iOS 锁屏时间下方的组件栏空间非常局促,通常只能容纳一个「矩形组件 + 两个圆形组件」或「四个圆形组件」。

如果把 4 个指标全部做成单独的圆形小组件,不仅放不下,而且信息极度零散;如果全挤进一个矩形组件,又失去了圆形仪表盘那种一眼感知负荷比例的直观感。

最终确定的排版策略是动静分离:

  • 动态高频指标:实时功率做成一个独立的圆形仪表小组件(Gauge),外圈圆环显示负荷比例,中心显示带 1 位小数的精确瓦数。
  • 静态低频指标:昨日电量、本月电量、账户余额三个结算数据,打包塞进一个矩形模板小组件(Template),三行文字一次性展示完毕。

矩形小组件配置(昨日 / 本月 / 余额 三合一)

长按 iPhone 锁屏进入编辑状态,点击时间下方的小组件区域,找到 Home Assistant,添加一个「矩形模板小组件」。

点击进入组件的详细配置页面,依次在三个文本框中填入对应的 Jinja2 模板代码。为了保证在 iOS 锁屏字体(San Francisco)下排版紧凑且不换行,单位统一使用 kWh¥ 代替中文字符。

1. 上部文本模板

用于展示昨日官方结算电量,保留 1 位小数:

代码段

昨日 {{ states('sensor.99_08eac3dd56454a88a6fbf021d00937cb_02_osg_uc_2608231221_002_3335_previous_daily_usage') | float(0) | round(1) }} kWh

2. 下部文本模板

用于展示本月迄今为止累计消耗的电量,保留 1 位小数:

代码段

本月 {{ states('sensor.99_08eac3dd56454a88a6fbf021d00937cb_02_osg_uc_2608231221_002_3335_current_month_usage') | float(0) | round(1) }} kWh

3. 详情文本模板 (仅限矩形系列)

用于展示当前电费账户的可用余额,保留 1 位小数:

代码段

余额 ¥{{ states('sensor.99_08eac3dd56454a88a6fbf021d00937cb_02_osg_uc_2608231221_002_3335_account_balance') | float(0) | round(1) }}

这三个模板中均加入了 | float(0) 作为容错处理。当国网插件在后台更新或网络出现瞬时断联导致实体变为 unavailableunknown 时,模板会默认赋予 0,避免底层渲染引擎抛出未捕获异常导致组件白屏。

圆形仪表小组件配置(当前实时功率)

在矩形组件旁边添加一个 Home Assistant 的「圆形仪表小组件(Gauge)」。这个组件由一个外圈进度环和中心文字组成,非常适合用来展示功率负载。

这里的量程上限设定为 3000W(可根据自己家里的入户主开关与常用大功率电器峰值调整)。点击小组件进入配置,分别填写 4 个输入框:

1. 值模板 (0-1)

该输入框决定了外圈进度环行进的比例。iOS 规范要求传入一个 0.0 到 1.0 之间的浮点数。用当前功率除以满量程 3000 即可:

代码段

{{ states('sensor.esp32_c3_p') | float(0) / 3000 }}

2. 值标签模板

该输入框决定了圆环正中心展示的文字内容。要求显示精准数值并带上 1 位小数及单位 W

代码段

{{ states('sensor.esp32_c3_p') | float(0) | round(1) }} W

3. 最小标签模板

仪表盘起始点的标注文本,填入纯文本即可:

Plaintext

0

4. 最大标签模板

仪表盘终点的满量程标注文本:

Plaintext

3000 W

调试踩坑与底层机制排查

在实际配置过程中,看似简单的几行模板却多次出现了小组件直接显示问号 ?、刻度满格爆表、以及修改后完全不刷新的情况。逐一排查后,把遇到的几个关键坑点记录如下:

1. HA 账号必须具备管理员权限(最隐蔽的核心前提)

这是最容易被忽略、但杀伤力最大的一个机制。

Home Assistant 官方在 iOS 客户端的架构设计中,将小组件内的 Jinja2 模板动态渲染归类为敏感操作。手机端 App 当前登录的用户账号,必须拥有 Administrator(管理员)权限

如果手机 App 登录的是一个普通受限用户(比如专门给家庭成员分配的只读账号),HA 后端在接收到 iOS 客户端发来的模板渲染请求时,会直接拦截并返回鉴权拒绝。结果就是 iOS 端拿不到任何数据,小组件失去渲染内容,直接在锁屏上显示为一个醒目的 ?

解决办法:进入 HA 网页端的「设置 -> 用户」,检查并确保移动端登录的账号开启了管理员权限。

2. 不要使用 Home Assistant 不支持的 Jinja2 语法

在做圆形仪表的值模板时,一开始为了防止数值超出 3000 导致比例大于 1.0,尝试在模板中加入了 | clamp(0, 1) 过滤器。

但这会导致小组件完全崩溃。原因是 Home Assistant 内置的 Jinja2 运行环境中并没有注册 clamp 过滤器,模板解析器在执行时会抛出 no filter named 'clamp' 错误。一旦模板报错,输出即为空值,组件同样会回退显示为 ?

实际上完全不需要在模板层做边界限制,直接写 {{ states('sensor.esp32_c3_p') | float(0) / 3000 }} 即可。iOS 系统的 WidgetKit 对 Gauge 组件底层自带截断保护,超出 1.0 会自动锚定在满环位置,不会导致 UI 溢出崩溃。

3. 为什么不建议用复杂的 format 字符串补零

在追求「固定保留一位小数(即便整十数也要显示为 .0)」时,常见的 Jinja2 写法是使用 {{ '%.1f' | format(...) }}

但在移动端小组件的极端场景下,如果实体由于网络断连短暂输出了非数值状态字符串,format 函数在某些版本的 HA 移动端桥接层中容易发生格式化类型不匹配异常。相比之下,使用 | float(0) | round(1) 具有极佳的向下兼容性和容错性,即便遇到异常状态也能稳定输出数字,不会破坏小组件的生命周期。

4. 频繁修改触发 iOS WidgetKit 的预算挂起

在反复调试模板格式、改一个字就锁屏看一眼的过程中,可能会发现小组件突然彻底停更,无论怎么改都卡在旧状态或者问号状态。

这是触发了 iOS 系统底层的「小组件刷新预算(WidgetKit Budget)」保护机制。iOS 系统的守护进程(dasd)会监控后台各个小组件的唤醒频率和耗电情况。如果短时间内频繁触发保存、渲染和推送,系统会判定 Home Assistant 小组件存在死循环或耗电异常,从而直接将其后台渲染进程强制挂起(冻结)。

遇到这种假死状态,不需要反复折腾配置:

  • 首先解锁进入手机,打开一次 Home Assistant App 并在主界面下拉刷新,强制建立前台通信。
  • 如果锁屏依然没有恢复,直接重启一次 iPhone。重启可以强制清空 iOS 的 WidgetKit 频控计数器和挂起标记,开机后组件就会立刻恢复正常拉取。

总结

从几年前用 ESP32 和 Modbus 模块接入 DDSU666 解决本地实时采集,到后来通过国网插件接入官方结算数据,再到现在将两路数据整合进 iOS 锁屏小组件,整个家庭用电监控才算达到了真正好用的状态。

现在拿起手机不用解锁,点亮屏幕就能扫一眼当前大功率电器的运行负荷,同时昨天用了多少度电、账户余额还剩多少也一目了然。对于工具流折腾来说,能把复杂的数据链路最终收敛成锁屏上两个轻巧的小模块,这种折腾就很有价值。