ESP8266+光电探头读取智能电表D0 SML数据,通过MQTT接入Home Assistant
2026/9/8 8:05:51 网站建设 项目流程

简介:SMLReader是一套基于ESP8266/ESP32的智能电表数据接入方案,面向物联网开发者与智能家居爱好者,解决D0 SML接口电力数据难以融入现有MQTT体系的问题。资源包包含完整Arduino/C++工程源码、头文件、配置文件与说明文档,共29个文件、大小839KB;其中png/jpg演示硬件接线与运行效果,h/cpp实现SML解析、OBIS识别与MQTT发布,readme/md提供编译与配置指南,还附带平台IO配置文件与持续集成脚本。已有267人学习。通过该项目可掌握ESP8266读取D0接口、解析SML信息结构、映射OBIS码并发布MQTT主题的完整链路,适合作为能源监控、家庭自动化DIY的参考模板,也能加深对智能电网通信协议的理解。 家里智能电表装好之后,我一开始只是当个“数字版本的电表”用,后来越看越心痒,想把这些数据实时接进 Home Assistant,做用电趋势和峰谷统计。市面上现成的方案不是贵就是封闭,直到我按 SMLReader 这个项目的思路折腾了一版:一块 ESP8266 开发板,配合电表 D0 数据口的光电探头,把智能电表通过 D0 SML 协议周期性推送的计量数据读出来、解析成可读数值,再以 MQTT 消息的形式发布到局域网里的 broker,这就是一个非常典型的“D0 SML 到 MQTT 网关”。这个方案硬件成本低、逻辑清楚,适合想搞家庭能源监控、又不想被厂商云平台绑死的人。

1. 为什么是“D0 SML + ESP8266 + MQTT”这个组合

1.1 先搞清电表那头输出的是什么

不看协议直接买模块,十有八九会踩坑。家里的智能电表通常不开放上网口走 Modbus,但基本都会留一个专门用于本地读数的数据口。欧洲和国内不少电表走的是 IEC 62056-21 标准里的 D0 口,数据格式则用 SML(Smart Message Language)来传输。

SML 是一种基于字节的二进制消息语言,电表会定时把当前电压、电流、功率、累计电量、分时电量等信息打包成一帧,从 D0 口发出来。D0 口在硬件上是 20mA 电流环,常见工作模式是电表主动推数据,也就是“只发不收”,所以我们的网关只需要单向读取就行。

对比其他读表方式:脉冲输出只能得到相对变化,要自己换算成电量,断电重启后还会丢基准;Modbus 接口往往不开放、需要账户权限;而 D0 SML 基本是“插上就能读”,数据完整且自带 OI(OBIS)对象标识,非常适合作家庭网关的第一手数据源。

1.2 ESP8266 为什么够用

有人上来就问“要不要上 ESP32、树莓派更稳?”说实话,单纯完成“SML 读取 + MQTT 发布”这个工作,ESP8266 绰绰有余。D0 SML 的数据速率通常只有 9600 波特,折算下来每秒不到 1KB,ESP8266 的 UART 完全跑得动。需要的算力也只是扫描帧头、提取字节、拼一下 JSON,这类任务用 ESP32 甚至有点浪费。

更重要的一点是 ESP8266 的生态太成熟了:Arduino 框架下直接操作 HardwareSerial 和 PubSubClient,十几行代码就能把 MQTT 跑起来;板子便宜,丢了不心疼;如果哪天固件写崩了,重新烧录也就几十秒的事。

选择 MQTT 而非 HTTP 的原因也很实际:电表是周期性或者变化时推送数据,MQTT 天然适合这种高频次、小体量的消息;在局域网里搭一个 Mosquitto 或者 EMQX,Home Assistant、Node-RED、Grafana 都可以通过订阅同一个 topic 拿到数据,不用给每个平台单独写一遍 HTTP 接口。可以说这个网关本质上是把“电表的二进制口”翻译成“家居平台能听懂的 MQTT 普通话”。

2. D0 硬件接入:最容易翻车的环节

2.1 光电探头模式还是串口直连模式

接入方式主要看电表提供的物理接口。D0 口常见两种形态:一种是表盘上有红外/光电窗口,需要用光电探头贴在表盘上读取;另一种是直接引出了串口或者接线端子,可以用杜邦线接到微控制器。

光电探头方案的安全性和便利性最好。现在网上有成品光电探头,用电工胶布贴在电表光电口上、对准窗口就行,不需要断电接线。这类探头内部已经把电流环信号转换成了 3.3V 或 5V 电平的 UART 信号,我实测接 Wemos D1 mini 的 RX 引脚就能直接读到数据。

接线端子方案则需要确认电平。D0 电流环输出的常见配置是 20mA 回路,有的厂家模块内部会做电平转换,直接输出 3.3V TTL;也有的是 5V TTL,这种情况下直接接 ESP8266 会存在过压风险,稳妥做法是串一个 1kΩ 电阻再分压,或者用光耦做一次隔离。我自己第一次踩坑就是拿 5V 输出直接怼到 GPIO3,板子倒是没烧,但偶尔会收到一堆乱码,后来加了一级电阻分压才彻底稳定。

注意:如果是外接端子,接线前务必确认电表 D0 接口的公共参考电平,不要想当然认为“GND 就是电池负极”。工业电流环经常有“高低端灌电流”的区别,接错了轻则读不到数据,重则烧板子。

2.2 供电和布线的细节

ESP8266 虽然功耗不高,但 WiFi 发射瞬间电流能到 300mA 以上。如果用一个质量一般的手机充电头或者电脑 USB 口供电,很容易出现“平时正常、一联网就重启”的诡异现象。我的建议是至少用 1A 以上输出、纹波小的 5V 适配器,并尽量缩短供电线长度。如果你打算长期挂在配电箱里,还可以考虑用 HLK-PM01 这类 AC-DC 模块从市电取电,但这里涉及强电操作,建议没有充分把握的朋友不要自己搞,安全第一。

布线方面,D0 光电探头到 ESP8266 的距离越短越好。虽然波特率不高,但是工业环境里配电箱附近可能有变频器、电磁阀这类干扰源,长线缆会引入噪声。实测中我把探头线控制在 50cm 以内,配合绞线,几乎没有误码。如果确实要拉长线,可以考虑用屏蔽双绞线,并把屏蔽层单端接地。

3. SML 数据帧到底长什么样

3.1 帧头帧尾、CRC 与消息类型

SML 帧不是直接用文本发出来的,而是一串二进制字节流。常见电表输出的帧结构是:

  • 帧头固定为1B 1B 1B 1B 01 01 01 01
  • 帧尾固定为1B 1B 1B 1B 1A 03 05
  • 帧体里可以有多条消息,最常见的两条是GetList.RequestGetList.Response
  • 部分厂商会在帧尾前附带 CRC16 校验字节

所以解析思路就清晰了:持续从串口读字节,寻找帧头序列,进入“缓存模式”,直到遇到帧尾序列,取中间数据按位解析即可。这就是 SML 解析器的核心状态机逻辑,并不需要真的把整棵 SML 树都实现出来。

每帧里真正有价值的数据,是GetList.Response消息中一串ListOf.ValueList条目。每个条目里包含一个 6 字节的 OBIS 编号,比如01 00 01 08 00 FF,展开后就是1-0:1.8.0*255,也就是“累计正向有功电能”。接下来跟着的是数值长度、数值类型和数值本身,按 big-endian 读取即可。

3.2 OBIS 对象与单位换算

不同电表推送的 OBIS 对象数量不一样,少则十几个,多则几十个。列几个最常用到的:

OBIS 代码含义常见单位
1-0:1.8.0*255累计正向有功电能kWh
1-0:2.8.0*255累计反向有功电能(光伏等)kWh
1-0:1.8.1*255T1 峰段电能kWh
1-0:1.8.2*255T2 谷段电能kWh
1-0:16.7.0*255当前总有功功率W
1-0:31.7.0*255L1 相电流A
1-0:51.7.0*255L2 相电流A
1-0:71.7.0*255L3 相电流A
1-0:32.7.0*255L1 相电压V
1-0:52.7.0*255L2 相电压V
1-0:72.7.0*255L3 相电压V

需要特别提醒:OBIS 编号基本是行业标准,但具体每个值的缩放系数受电表厂商影响。比如同样是累计电能,有的表直接发 Wh,有的表发 1/1000 Wh,还有的发带符号的 int64。偷懒做法是直接把 8 字节转成 int64 后除以 1000000,但严谨做法是解析 SML 条目里的单位字段和 scaler 字段,用它们决定小数位的偏移。

我自己调试时是先对拍电表屏幕,把功率、电压的原始字节和显示值做对比,确认缩放系数后再硬编码进配置。毕竟家庭场景里电表型号固定,不需要做通用抽象。

3.3 一段极简的解析思路

在 ESP8266 上跑完整 SML 树解析库不是不行,但资源开销偏高。我的做法是在扫描到帧头后,将帧体按字节缓存,然后开始“找 OBIS”:

// 伪代码,示意解析思路 if (findPattern(buf, {0x01, 0x00, 0x01, 0x08, 0x00, 0xFF})) { // 向后读 1 字节 length,判断类型后读取 8 字节 big-endian int64_t energy = readBigEndianInt64(buf, pos); energyWh = energy / 1000.0; // 按实际表计缩放 }

实际工程里要注意处理“一帧里同一个 OBIS 出现多次”的情况,比如分时电能会同时存在 1.8.0、1.8.1、1.8.2。我的做法是按 OBIS 代码作为 map 的 key,后出现的值覆盖先出现的值,最后统一发布。

4. 固件实现与 MQTT 发布

4.1 串口读取与帧扫描状态机

我用的开发板是 WeMos D1 Mini,UART0 RX 在 GPIO3。接好光电探头后,直接初始化串口:

Serial.begin(9600, SERIAL_8N1); // 如果读不到数据,可以尝试 SERIAL_8E1 或 SERIAL_7E1

SML 默认帧速是 9600 8N1,但个别表会先以 300 波特率发送协商指令再切到 9600,这种就需要额外做波特率切换逻辑。我在实际调试中遇到的情况是电表直接以 9600 持续推送,因此没有做 300 波特率协商,如果你遇到“完全没有数据”,可以拿一个 USB-TTL 先接电脑,用串口工具抓一下原始字节流,确认实际波特率再下手。

帧扫描状态机用四个状态即可:空闲、准备帧头、接收中、帧结束。帧头检测用滑动窗口比对,不需要等到完整帧头才记录,因为电表可能从帧中间开始发送,前几个字节是残缺的。

uint8_t minorState = 0; const uint8_t SM_HEADER[] = {0x1B,0x1B,0x1B,0x1B,0x01,0x01,0x01,0x01}; const uint8_t SM_END[] = {0x1B,0x1B,0x1B,0x1B,0x1A,0x03,0x05}; bool checkPattern(uint8_t *buf, int len, const uint8_t *pattern, int plen) { if (len < plen) return false; for (int i = 0; i < plen; i++) if (buf[len - plen + i] != pattern[i]) return false; return true; }

用类似checkPattern的方法,在每次收到新字节时把数据追加到 ring buffer 并检查后缀,就能在内存占用极小的情况下完成帧采集。帧结束后再交给解析函数,解析完清空缓冲区等待下一帧。

4.2 Topic 设计与 JSON 负载

MQTT 网关不只是“把数据发出去”就完了,topic 和负载设计直接影响后面 Home Assistant 或者 Node-RED 用起来顺不顺手。我的建议是采用层级结构,把站点和表计分开:

  • tele/electricity/SMLReader/raw:完整原始 JSON,便于调试
  • tele/electricity/SMLReader/energy:只包含电能相关值
  • tele/electricity/SMLReader/power:只包含功率、电压、电流瞬时值

负载用 JSON 字符串,示例:

{ "timestamp": 1742188800, "power_w": 286.5, "energy_total_kwh": 12345.678, "energy_t1_kwh": 9876.5, "energy_t2_kwh": 2469.178, "voltage_l1_v": 231.2, "current_l1_a": 1.24 }

发布时保留retain标志,这样 Home Assistant 重启订阅后能立刻拿到最新值,不用等下一帧推送。如果担心刷屏,可以在 ESP8266 端做一个去重缓存,只有数值变化超过阈值时才发送,不过我在实际中直接保底“每 10 秒发布一次总数据”,简单直接,也足够实时。

4.3 对接 Home Assistant

如果不想暴露一堆原始 JSON topic,推荐直接使用 MQTT Discovery。ESP8266 连接成功后,向homeassistant/sensor/electricity/config发布一段 JSON,Home Assistant 会自动创建一个叫“electricity”的传感器:

{ "name": "Electricity Meter", "state_topic": "tele/electricity/SMLReader/energy", "value_template": "{{ value_json.energy_total_kwh }}", "unit_of_measurement": "kWh", "device_class": "energy", "unique_id": "smlreader_energy_total" }

这样在 Lovelace 界面里就能直接看到一个实时变化的电量卡片。我另外建了功率、电压的 discovery,展示效果更丰富,还能配合 InfluxDB + Grafana 做长期历史趋势。

4.4 重连和看门狗

家庭网络环境里 ESP8266 最容易出的问题就是 WiFi 掉线后不自动重连,或者连着连着 MQTT 不发了。我的处理是三层保活:

  • WiFi 层:启用wifi_station_set_hostname()并设置静态 IP,减少 DHCP 交互风险
  • MQTT 层:setKeepAlive(15),回调里判断连接断开后每 5 秒尝试重连,且设置client.disconnect()清理旧连接
  • 系统层:在loop()中实时检测网络断开时长,超过 60 秒没有恢复就主动ESP.restart()

另外,ESP8266 默认的 WiFi 省电模式会经常进入 sleep,导致延迟高甚至丢包。实测在 SMLReader 这种需要持续实时性的场景里,我把WiFi.setSleep(false)关掉省电模式后,丢包率明显下降。

5. 常见问题与排查实录

5.1 完全读不到数据

先别怀疑代码,先怀疑物理链路。用电脑 USB-TTL 接 D0 探头,打开串口监视器,波特率 9600,看能不能看到类似1B 1B 1B 1B 01 01 01 01的原始字节。如果电脑上都看不到,说明接线方向、电平、探头位置有问题。

常见的几个坑是:光电探头没有对准电表光学窗口、胶带没贴严实漏光、电表 D0 端子需要短接某个使能引脚才输出、波特率是 2400 或 300。排查顺序我建议是“电脑串口工具先确认物理链路,再轮到 ESP8266 固件”。

5.2 能读到帧头,但解析出的数值是乱码

多半是电平或串口参数不匹配。有一种很隐蔽的情况是 D0 输出的逻辑电平反相,也就是空闲时为低、有数据时为高,普通 UART 解析会得到明显乱码。这种可以加一个逻辑非门或者用光耦反向电路解决;有的模块也会提供“开漏输出”模式,配合上位机串口的外部上拉就能纠正。

如果只是偶发的乱码,则可能是供给 ESP8266 的电源纹波太大,或者光电探头引线太长收到干扰。我给配电箱内网关加了一颗 100μF 电解电容和 0.1μF 瓷片电容并联,干扰问题就消失了。

5.3 数值对不上、电能总是差距一个数量级

这类问题的核心是缩放系数没对上。SML 里的数值虽然是 int64,但单位可能不一样。我遇到过功率直接以 W 为单位输出,也遇到过以 10^-6 kW 为单位输出的表,如果代码里写死除以 1000,就会差 1000 倍。

排查方法是手动抓一帧,对比屏幕上同时刻显示的值和原始字节转出的十进制数,确定比例关系。如果出现负的累计电能,注意符号位扩展,int64_tuint64_t时高位符号会带来负数结果,必要时做绝对值处理或者预留符号判断。

5.4 WiFi 丢包和 MQTT 掉线

ESP8266 的 WiFi 丢包原因很多,但家庭网关场景下最常见是供电不足和省电模式。我建议先用固定电源代替劣质 USB 口供电,再关掉 WiFi 休眠,最后检查路由器是否开启了“AP 隔离”这类阻止局域网互访的功能。

如果 MQTT 掉线频繁,可以观察 broker 的日志。常见原因是客户端没有在 keepalive 周期内发 PINGREQ,因为主循环被delay()长时间阻塞。解决办法是把发布逻辑改成分时调度,避免一个耗时的 MQTT publish 阻塞串口读取,我甚至把解析和发布拆成了两个阶段:串口中断收帧、主循环发布。

6. 最后说点实际体会

E家搞能源监控以来,这个 D0 SML 网关是我用过的几个方案里最省心的。虽然前后折腾了三个晚上,主要时间花在电平适配和缩放系数标定上,但一旦跑稳,后续基本不用管。整个过程最大的心得是:遇到协议类项目,不要一开始就抱着“完整实现标准”的心态,先抓关键字节、跑通最小闭环,再慢慢补充功能和容错,这样推进速度会快很多。

另外,如果你不想维护自写固件,ESPHome 里也有现成的 sml 组件,可以直接把光电探头接到支持 UART 的 ESP32/ESP8266 上,配置一段 YAML 就能接入 Home Assistant。自写固件的好处是可控性强、代码量少、方便按自己需求二次开发,看你的目标了。总之,D0 SML 到 MQTT 这条路我已经帮你蹚过一遍,想复刻的朋友直接从硬件接线和串口抓包开始,很快就能见到第一帧电表数据在 MQTT topic 里出现。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询