跳转到内容

Modbus 控制器

modbus_controller 组件创建一条 RS485 连接来控制 Modbus 服务器(从站)设备,让你的 ESPHome 节点充当 Modbus 客户端(主站)。你可以将设备上的线圈、输入、保持寄存器、读取寄存器作为传感器、开关、选择器、数字或其他各种 ESPHome 组件来访问,并将它们呈现给你最喜欢的家庭自动化系统。你甚至可以从 ESPHome 以二进制或浮点输出的形式写入它们。

将 modbus_controller 组件所依赖的 Modbus 的 role 属性设置为 client,这是默认值。

你需要一个 RS485 收发器模块:

有关更多细节,请参阅 stackexchange 上的这个 RS485 模块是如何工作的?。

收发器连接到 MCU 的 UART。对于 ESP32,默认将引脚 16 连接到 TXD、引脚 17 连接到 RXD,但也可以使用任何其他引脚。将 3.3V 连接到 VCC,当然还有 GND 连接到 GND。

在总线侧,根据 Modbus 标准,你需要在总线电缆的两端各放置一个 120 欧姆的终端电阻。有些收发器已经将其焊接到板上,而一些从站设备可能通过跳线或 DIP 开关提供。

NOTE

如果你使用的是 ESP8266,串行日志记录可能会导致从 UART 读取出现问题。为获得最佳效果,建议使用硬件串行。如果其他组件在 loop() 中花费大量时间,软件串行可能无法读取所有接收到的数据。

对于硬件串行,只能使用一组有限的引脚。使用 tx_pin: GPIO1 和 rx_pin: GPIO3,或 tx_pin: GPIO15 和 rx_pin: GPIO13。

使用硬件 UART 的缺点是不能再使用串行日志记录,因为串行日志会被发送到 Modbus 设备(们),从而导致错误。

可以通过设置 baud_rate: 0 来禁用串行日志记录。

参见 Logger 了解更多细节。

logger:
level: <level>
baud_rate: 0
  • modbus_id (可选, ID): 手动指定 modbus hub 的 ID。

  • address (必选, ID): 从站设备的 Modbus 地址。

  • allow_duplicate_commands (可选, boolean): 是否允许队列中出现重复命令。默认为 false。

  • command_throttle (可选, Time): 对设备进行 2 次请求之间的最短时间。默认为 0ms。 某些 Modbus 从站设备会限制来自主站的请求速率,因此此选项允许改变请求之间的间隔。

  • update_interval (可选, Time): 传感器检查的间隔。 默认为 60 秒。

  • offline_skip_updates (可选, integer): 当从站不响应命令时,它会被 标记为离线。你可以指定在离线期间要跳过的更新次数。如果总线连接了多个 从站,这可以避免等待超时,从而允许读取同一总线上的其他从站。当从站 响应命令时,它将再次被标记为在线。

  • max_cmd_retries (可选, integer): 如果没有收到响应,命令将重试多少次。不包括初始传输。默认为 4。

自动化:

以下代码创建一个 modbus_controller hub,与地址为 1、波特率为 115200 bps 的 ModBUS 设备通信。

ModBUS 传感器可以直接定义(内联)在 modbus_controller hub 下,或作为独立组件。 从技术上来说,“内联”定义和标准定义方法之间没有区别。

# 示例配置条目
uart:
...
modbus:
flow_control_pin: GPIOXX
id: modbus1
modbus_controller:
- id: modbus_device
address: 0x1 ## 总线上 Modbus 从站设备的地址
modbus_id: modbus1
setup_priority: -10
sensor:
- platform: modbus_controller
modbus_controller_id: modbus_device
name: "Battery Capacity"
register_type: holding
address: 0x9001 ## Modbus 从站设备内寄存器的地址
unit_of_measurement: "AH"
value_type: U_WORD
switch:
- platform: modbus_controller
modbus_controller_id: modbus_device
name: "Reset to Factory Default"
register_type: coil
address: 0x15
bitmask: 1
text_sensor:
- name: "rtc_clock"
platform: modbus_controller
modbus_controller_id: modbus_device
id: rtc_clock
internal: true
register_type: holding
address: 0x9013
register_count: 3
raw_encode: HEXBYTES
response_size: 6

上面的配置示例创建了一个 modbus_controller hub,与地址为 1、波特率为 115200 bps 的 Modbus 设备通信,实现了一个传感器、一个开关和一个文本传感器。

以下代码允许 ModBUS 客户端从你的 ESPHome 节点读取传感器值,而该节点本身是从 ModBUS 服务器读取的。

uart:
- id: uart_modbus_client
tx_pin: 32
rx_pin: 34
- id: uart_modbus_server
tx_pin: 25
rx_pin: 35
modbus:
- uart_id: uart_modbus_client
id: modbus_client
- uart_id: uart_modbus_server
id: modbus_server
role: server
modbus_controller:
- id: modbus_evse
modbus_id: modbus_client
address: 0x2
update_interval: 5s
modbus_server:
- modbus_id: modbus_server
address: 0x4
server_registers:
- address: 0x0002
value_type: S_DWORD_R
read_lambda: |-
return id(evse_voltage_l1).state;
sensor:
- platform: modbus_controller
id: evse_voltage_l1
modbus_controller_id: modbus_evse
name: "EVSE voltage L1"
register_type: holding
address: 0x0000
device_class: voltage
value_type: S_DWORD_R
accuracy_decimals: 1
unit_of_measurement: V
filters:
- multiply: 0.1

请查看文档底部的 .. _modbusseealso: 一节中提供的各种 Modbus 组件。它们可以直接定义(内联)在 modbus_controller hub 下,或作为独立组件。从技术上来说,内联定义和标准定义方法之间没有区别。

下面你会找到一些关于在更高级场景中使用 Modbus 的一般提示。适用的组件功能都有指向这里的链接:

有些设备在读取寄存器中使用十进制值来表示多个二进制状态,这些状态只占用一个寄存器地址。要解码它们,你可以根据下表使用位掩码。与某位对应的十进制值始终是其所在行的前一个值的两倍。将对应各个位的所有值相加,就可以在单个寄存器中表示多个位。

Alarm 位描述DEC 值HEX 值
bit 0二进制传感器 011
bit 1二进制传感器 122
bit 2二进制传感器 244
bit 3二进制传感器 388
bit 4二进制传感器 41610
bit 5二进制传感器 53220
bit 6二进制传感器 66440
bit 7二进制传感器 712880
bit 8二进制传感器 8256100
bit 9二进制传感器 9512200
bit 10二进制传感器 101024400
bit 11二进制传感器 112048800
bit 12二进制传感器 1240961000
bit 13二进制传感器 1381922000
bit 14二进制传感器 14163844000
bit 15二进制传感器 15327688000

在下面的示例中,寄存器 15 保存了多个二进制值。它存储十进制值 12288,即 4096 + 8192 的和,这意味着对应的位 12 和 13 为 1,其他位为 0。

要在 ESPHome 中将这些位中的一些作为二进制传感器收集,请使用 bitmask:

binary_sensor:
- platform: modbus_controller
modbus_controller_id: modbus1
name: Alarm bit0
register_type: read
address: 15
bitmask: 0x1
- platform: modbus_controller
modbus_controller_id: modbus1
name: Alarm bit1
register_type: read
address: 15
bitmask: 0x2
- platform: modbus_controller
modbus_controller_id: modbus1
name: Alarm bit10
register_type: read
address: 15
bitmask: 0x400
- platform: modbus_controller
modbus_controller_id: modbus1
name: Alarm bit15
register_type: read
address: 15
bitmask: 0x8000

custom_command 可用于创建任意的 modbus 命令。与 lambda 结合使用,可以处理任何响应。 本示例重新实现了从 SDM-120 读取寄存器 0x156(总有功电能)和 0x158(总无功电能)的命令。 SDM-120 返回浮点值,使用 2 个寄存器的 32 位。

uart:
id: mod_uart
...
modbus:
send_wait_time: 200ms
uart_id: mod_uart
id: mod_bus
modbus_controller:
- id: sdm
address: 2
modbus_id: mod_bus
command_throttle: 100ms
setup_priority: -10
update_interval: 30s
sensors:
- platform: modbus_controller
modbus_controller_id: sdm
name: "Total active energy"
id: total_energy
# address: 0x156
# register_type: "read"
## 使用 custom_command 重新实现
# 0x2 : modbus 设备地址
# 0x4 : modbus 功能码
# 0x1 : modbus 寄存器地址的高字节
# 0x56: modbus 寄存器地址的低字节
# 0x00: 请求的寄存器总数的高字节
# 0x02: 请求的寄存器总数的低字节
custom_command: [ 0x2, 0x4, 0x1, 0x56,0x00, 0x02]
value_type: FP32
unit_of_measurement: kWh
accuracy_decimals: 1
- platform: modbus_controller
modbus_controller_id: sdm
name: "Total reactive energy"
# address: 0x158
# register_type: "read"
custom_command: [0x2, 0x4, 0x1, 0x58, 0x00, 0x02]
## 该命令返回浮点值,使用 4 个字节
lambda: |-
ESP_LOGD("Modbus Sensor Lambda","Got new data" );
union {
float float_value;
uint32_t raw;
} raw_to_float;
if (data.size() < 4 ) {
ESP_LOGE("Modbus Sensor Lambda", "invalid data size %d",data.size());
return NAN;
}
raw_to_float.raw = data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3];
ESP_LOGD("Modbus Sensor Lambda", "FP32 = 0x%08X => %f", raw_to_float.raw, raw_to_float.float_value);
return raw_to_float.float_value;
unit_of_measurement: kVArh
accuracy_decimals: 1

register_count 是一个仅在不常见的响应编码或优化 modbus 通信时才需要的选项。

它描述了此数据点跨越的寄存器数量,覆盖由 value_type 确定的默认值。如果没有提供 register_count 的值,则根据寄存器类型计算。一个寄存器的默认大小为 16 位(一个字)。有些设备不遵守此约定,其寄存器大于 16 位。在这种情况下,必须设置 register_count 和 response_size。例如,如果你的 Modbus 设备用一个寄存器存放一个 FP32 值(而不是默认的两个),请设置 register_count: 1 和 response_size: 4。

register_count 也可以用来在连续的寄存器范围内跳过一定数量的寄存器。

一个例子是 SDM 电表,在寄存器地址 0、2、4 和 6 处有感兴趣的数据:

- platform: modbus_controller
name: "Voltage Phase 1"
address: 0
register_type: "read"
value_type: FP32
- platform: modbus_controller
name: "Voltage Phase 2"
address: 2
register_type: "read"
value_type: FP32
- platform: modbus_controller
name: "Voltage Phase 3"
address: 4
register_type: "read"
value_type: FP32
- platform: modbus_controller
name: "Current Phase 1"
address: 6
register_type: "read"
value_type: FP32
accuracy_decimals: 1

上面的配置将生成一条 modbus 命令从 0 到 6 读取多个寄存器。

也许你不在乎寄存器地址 2 和 4(Phase 2 和 Phase 3 的电压值)中的数据(或者你有一个 SDM-120)。 当然,你可以删除你不关心的传感器,但那样地址中就会出现缺口。如果你移除地址 2 和 4 处的寄存器,将生成两条命令——读取寄存器 0 和读取寄存器 6。为了避免生成多条命令从而减少总线上的活动,可以使用 register_count 来填充缺口:

- platform: modbus_controller
name: "Voltage Phase 1"
address: 0
unit_of_measurement: "V"
register_type: "read"
value_type: FP32
register_count: 6
- platform: modbus_controller
name: "Current Phase 1"
address: 6
register_type: "read"
value_type: FP32

因为第一个传感器使用了 register_count: 6 选项,将只使用一条命令从 0 到 6 读取多个寄存器,但中间的值将被忽略。

NOTE

计算: FP32 是 32 位值,占用 2 个寄存器。因此,要跳过 2 个 FP32 寄存器,必须将这 2 个寄存器的大小添加到第一个寄存器的默认大小中。所以地址 0 占用 2、地址 2 占用 2、地址 4 占用 2,因此 register_count 必须为 6。

sensors:
- platform: modbus_controller
modbus_controller_id: epever
id: array_rated_voltage
name: "array_rated_voltage"
address: 0x3000
unit_of_measurement: "V"
register_type: read
value_type: U_WORD
accuracy_decimals: 1
skip_updates: 60
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: epever
id: array_rated_current
name: "array_rated_current"
address: 0x3001
unit_of_measurement: "V"
register_type: read
value_type: U_WORD
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: epever
id: array_rated_power
name: "array_rated_power"
address: 0x3002
unit_of_measurement: "W"
register_type: read
value_type: U_DWORD_R
accuracy_decimals: 1
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: epever
id: battery_rated_voltage
name: "battery_rated_voltage"
address: 0x3004
unit_of_measurement: "V"
register_type: read
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: epever
id: battery_rated_current
name: "battery_rated_current"
address: 0x3005
unit_of_measurement: "A"
register_type: read
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: epever
id: battery_rated_power
name: "battery_rated_power"
address: 0x3006
unit_of_measurement: "W"
register_type: read
value_type: U_DWORD_R
accuracy_decimals: 1
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: epever id: charging_mode
name: "charging_mode"
address: 0x3008
unit_of_measurement: ""
register_type: read
value_type: U_WORD
accuracy_decimals: 0

为最大限度地减少所需的事务,所有具有相同基地址的寄存器都在一次请求中读取。 响应根据 register_count 和字节偏移量映射到传感器。例如:

请求:

data描述
0x1 (01)设备地址
0x4 (04)功能码 4(读取输入寄存器)
0x30 (48)起始地址高字节
0x0 (00)起始地址低字节
0x0 (00)要读取的寄存器数高字节
0x9 (09)要读取的寄存器数低字节
0x3f (63)crc
0xc (12)crc

响应:

offsetdatavalue (type)描述
H0x1 (01)设备地址
H0x4 (04)功能码
H0x12 (18)字节数
00x27 (39)U_WORDarray_rated_voltage 高字节
10x10 (16)0x2710 (100000)array_rated_voltage 低字节
20x7 (7)U_WORDarray_rated_current 高字节
30xd0 (208)0x7d0 (2000)array_rated_current 低字节
40xcb (203)U_DWORD_Rarray_rated_power 低字的高字节
50x20 (32)spans 2 registerarray_rated_power 低字的低字节
60x0 (0)array_rated_power 高字的高字节
70x0 (0)0x0000CB20 (52000)array_rated_power 高字的低字节
80x9 (09)U_WORDbattery_rated_voltage 高字节
90x60 (96)0x960 (2400)battery_rated_voltage 低字节
100x7 (07)U_WORDbattery_rated_current 高字
110xd0 (208)0x7d0 (2000)battery_rated_current 高字
120xcb (203)U_DWORD_Rbattery_rated_power 低字的高字节
130x20 (32)spans 2 registerbattery_rated_power 低字的低字节
140x0 (0)battery_rated_power 高字的高字节
150x0 (0)0x0000CB20 (52000)battery_rated_power 高字的低字节
160x0 (0)U_WORDcharging_mode 高字节
170x2 (02)0x2 (MPPT)charging_mode 低字节
C0x2f (47)crc
C0x31 (49)crc

NOTE

写入支持仅为开关和选择器实现;不过,C++ 代码提供了写入 Modbus 设备所需的 API。

这些方法可以从 lambda 调用。

下面是如何为 EPEVER Trace AN 控制器设置配置值的示例。 该代码将 MCU 的本地时间同步到 epever 控制器。 通过向寄存器 0x9013 写入 12 个字节来设置时间。 然后发送电池充电设置。

此示例需要一个已配置的 time 组件(例如 homeassistant、sntp)。

esphome:
on_boot:
## 在设置时配置控制器设置
## 确保优先级低于 modbus_controller 的 setup_priority
priority: -100
then:
- lambda: |-
// 获取本地时间并同步到控制器
auto time = ESPTime::from_epoch_local(::time(nullptr));
int seconds = time.second;
int minutes = time.minute;
int hour = time.hour;
int day = time.day_of_month;
int month = time.month;
int year = time.year % 100;
esphome::modbus_controller::ModbusController *controller = id(epever);
// 如果时间尚未同步,则跳过 RTC 更新
if (time.is_valid()) {
// 创建有效负载
std::vector<uint16_t> rtc_data = {uint16_t((minutes << 8) | seconds), uint16_t((day << 8) | hour),
uint16_t((year << 8) | month)};
// 以时间信息为有效负载创建一条 Modbus 命令项
esphome::modbus_controller::ModbusCommandItem set_rtc_command =
esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller, 0x9013, 3, rtc_data);
// 将命令提交到发送队列
epever->queue_command(set_rtc_command);
ESP_LOGI("ModbusLambda", "EPSOLAR RTC set to %02d:%02d:%02d %02d.%02d.%04d", hour, minutes, seconds, day, month,
year + 2000);
}
// Battery settings
// 注意:这些值仅为示例,适用于我的 AGM 电池
std::vector<uint16_t> battery_settings1 = {
0, // 9000 Battery Type 0 = User
0x0073, // 9001 Battery Cap 0x55 == 115AH
0x012C, // 9002 Temp compensation -3V /°C/2V
0x05DC, // 9003 0x5DC == 1500 Over Voltage Disconnect Voltage 15,0
0x058C, // 9004 0x58C == 1480 Charging Limit Voltage 14,8
0x058C, // 9005 Over Voltage Reconnect Voltage 14,8
0x05BF, // 9006 Equalize Charging Voltage 14,6
0x05BE, // 9007 Boost Charging Voltage 14,7
0x0550, // 9008 Float Charging Voltage 13,6
0x0528, // 9009 Boost Reconnect Charging Voltage 13,2
0x04C4, // 900A Low Voltage Reconnect Voltage 12,2
0x04B0, // 900B Under Voltage Warning Reconnect Voltage 12,0
0x04BA, // 900c Under Volt. Warning Volt 12,1
0x04BA, // 900d Low Volt. Disconnect Volt. 11.8
0x04BA // 900E Discharging Limit Voltage 11.8
};
// 增压与均衡时段
std::vector<uint16_t> battery_settings2 = {
0x0000, // 906B Equalize Duration (min.) 0
0x0075 // 906C Boost Duration (aka absorb) 117 mins
};
esphome::modbus_controller::ModbusCommandItem set_battery1_command =
esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller, 0x9000, battery_settings1.size() ,
battery_settings1);
esphome::modbus_controller::ModbusCommandItem set_battery2_command =
esphome::modbus_controller::ModbusCommandItem::create_write_multiple_command(controller, 0x906B, battery_settings3.size(),
battery_settings2);
delay(200) ;
controller->queue_command(set_battery1_command);
delay(200) ;
controller->queue_command(set_battery2_command);
ESP_LOGI("ModbusLambda", "EPSOLAR Battery set");
uart:
id: mod_bus
tx_pin: GPIOXX
rx_pin: GPIOXX
baud_rate: 115200
stop_bits: 1
modbus:
#flow_control_pin: GPIOXX
send_wait_time: 200ms
id: mod_bus_epever
modbus_controller:
- id: epever
## the Modbus device addr
address: 0x1
modbus_id: mod_bus_epever
command_throttle: 0ms
setup_priority: -10
update_interval: ${updates}
sensor:
- platform: modbus_controller
modbus_controller_id: epever
id: array_rated_voltage
name: "array_rated_voltage"
address: 0x3000
unit_of_measurement: "V"
register_type: read
value_type: U_WORD
accuracy_decimals: 1
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: epever
id: array_rated_current
name: "array_rated_current"
address: 0x3001
unit_of_measurement: "A"
register_type: read
value_type: U_WORD
accuracy_decimals: 2
filters:
- multiply: 0.01
- platform: modbus_controller
modbus_controller_id: epever
id: array_rated_power
name: "array_rated_power"
address: 0x3002
unit_of_measurement: "W"
register_type: read
value_type: U_DWORD_R
accuracy_decimals: 1
filters:
- multiply: 0.01

当 modbus_controller 发送了一条命令时,将触发此自动化。在 Lambdas 中,你可以在 function_code 中获取功能码,在 address 中获取寄存器地址。

modbus_controller:
- id: modbus_con
# ...
on_command_sent:
then:
- number.increment: modbus_commands

当 modbus_controller 在经历 offline(离线)状态之后变为 online(在线)时,将触发此自动化。在 Lambdas 中,你可以在 function_code 中获取功能码,在 address 中获取寄存器地址。

modbus_controller:
- id: modbus_con
# ...
on_online:
then:
- logger.log: "Controller back online!"

当 modbus_controller 变为 offline(离线)时,将触发此自动化(参见 offline_skip_updates)。在 Lambdas 中,你可以在 function_code 中获取功能码,在 address 中获取寄存器地址。

modbus_controller:
- id: modbus_con
# ...
on_offline:
then:
- logger.log: "Controller goes offline!"