跳转到内容

Modbus 控制器

modbus_controller 组件创建一个 RS485 连接,用于:

  • 控制 Modbus 服务器(从机)设备,让您的 ESPHome 节点充当 Modbus 客户端(主机)。您可以将设备上的线圈、输入、保持寄存器、只读寄存器作为传感器、开关、选择器、数值或各种其他 ESPHome 组件来访问,并将它们呈现给您最喜欢的家庭自动化系统。您甚至可以将它们作为二进制或浮点输出来写入。
  • 让您的 ESPHome 节点充当 Modbus 服务器,允许 Modbus 客户端从您的 ESPHome 节点读取数据(如传感器值)。

要选择角色,请设置此 modbus_controller 组件所依赖的 Modbusrole 属性。默认值为 client

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

有关更多详细信息,请参阅 Stack Exchange 上的 How is this RS485 module working?

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

在总线侧,根据 Modbus 标准,您需要在总线电缆的末端添加 120 欧姆的终端电阻。一些收发器已经在板上焊接了电阻,而一些从机设备可能通过跳线或 DIP 开关提供电阻。

NOTE

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

对于硬件串口,只能使用有限的引脚集。要么使用 tx_pin: GPIO1rx_pin: GPIO3,要么使用 tx_pin: GPIO15rx_pin: GPIO13

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

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

有关更多详细信息,请参阅 日志记录器

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

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

  • allow_duplicate_commands (可选, 布尔值): 是否允许队列中有重复命令。默认为 false

  • command_throttle (可选, 时间): 向设备发送请求之间的最小时间间隔。默认为 0ms。 一些 Modbus 从机设备限制了主机的请求速率,因此这允许更改请求之间的间隔。

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

  • offline_skip_updates (可选, 整数): 当从机没有响应命令时,它会被 标记为离线,您可以指定在它离线时要跳过的更新次数。如果使用有多个从机的总线,这可以避免等待超时,从而允许在同一条总线上读取其他从机。当从机 响应命令时,它会再次被标记为在线。

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

  • server_courtesy_response (可选): 配置块,用于在设备充当 Modbus 服务器时启用礼貌响应功能。

    • enabled (可选, 布尔值): 是否启用礼貌响应功能。 默认为 false
    • register_last_address (可选, 整数): 最高 Modbus 寄存器地址(包含),在此范围内允许读取未定义的寄存器并将使用默认值填充。 任何包含此范围内未定义寄存器的读取请求都将返回 register_value 指定的值,而不是触发异常。 默认为 65535
    • register_value (可选, 整数): 为 register_last_address 定义的地址范围内的未定义寄存器返回的 16 位值(范围:0–65535)。 默认为 0
  • server_registers (可选): 充当服务器时响应的寄存器列表。

    • address (必需, 整数): 范围内第一个寄存器的起始地址

    • value_type (可选): mod_bus 寄存器数据的数据类型。ModBUS 的默认数据类型是 大端序 格式的 16 位整数(网络字节顺序,MSB 在前)

      • U_WORD : 无符号 16 位整数,1 个寄存器,uint16_t
      • S_WORD : 有符号 16 位整数,1 个寄存器,int16_t
      • U_DWORD : 无符号 32 位整数,2 个寄存器,uint32_t
      • S_DWORD : 有符号 32 位整数,2 个寄存器,int32_t
      • U_DWORD_R : 小端序 无符号 32 位整数,2 个寄存器,uint32_t
      • S_DWORD_R : 小端序 有符号 32 位整数,2 个寄存器,int32_t
      • U_QWORD : 无符号 64 位整数,4 个寄存器,uint64_t
      • S_QWORD : 有符号 64 位整数,4 个寄存器 int64_t
      • U_QWORD_R : 小端序 无符号 64 位整数,4 个寄存器,uint64_t
      • S_QWORD_R : 小端序 有符号 64 位整数,4 个寄存器 int64_t
      • FP32 : 32 位 IEEE 754 浮点数,2 个寄存器,float
      • FP32_R : 小端序 32 位 IEEE 754 浮点数,2 个寄存器,float

      默认为 U_WORD

    • read_lambda (必需, lambda): 返回此寄存器值的 Lambda。

    • write_lambda (可选, lambda): 设置此寄存器值的 Lambda。提供了一个适当类型的变量 xuint16_tint32_t 等,见上文),其值为要写入的值, 以及包含此寄存器地址的 address。如果操作成功,您必须返回 true,否则返回 false,在这种情况下 将向客户端发送 ModBUS 异常代码 4

自动化:

  • on_command_sent (可选, 自动化): 当 modbus 命令发送后执行的自动化。请参阅 on_command_sent
  • on_online (可选, 自动化): 当 modbus 控制器上线时执行的自动化。请参阅 on_online
  • on_offline (可选, 自动化): 当 modbus 控制器离线时执行的自动化。请参阅 on_offline

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

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

# 示例配置条目
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 集线器,与地址为 1、波特率为 115200 的 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_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

查看文档底部 .. _modbus_controller-automations: 部分中可用的各种 Modbus 组件。它们可以直接定义*(内联)*在 modbus_controller 集线器下,也可以作为独立组件。从技术上讲,内联和标准定义方法之间没有区别。

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

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

报警位描述十进制值十六进制值
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 的和,意味着对应的位 12131,其他位为 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_countresponse_size。例如,如果您的 Modbus 设备使用一个寄存器存储 FP32 值(而不是默认的两个),请设置 register_count: 1response_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 和字节偏移量映射到传感器。例如:

请求:

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

响应:

偏移量数据值(类型)描述
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)跨越 2 个寄存器array_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)跨越 2 个寄存器battery_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 字节来设置的。 然后发送电池充电设置。

esphome:
on_boot:
## 在设置时配置控制器设置
## 确保优先级低于 modbus_controller 的 setup_priority
priority: -100
then:
- lambda: |-
// 获取本地时间并同步到控制器
time_t now = ::time(nullptr);
struct tm *time_info = ::localtime(&now);
int seconds = time_info->tm_sec;
int minutes = time_info->tm_min;
int hour = time_info->tm_hour;
int day = time_info->tm_mday;
int month = time_info->tm_mon + 1;
int year = time_info->tm_year % 100;
esphome::modbus_controller::ModbusController *controller = id(epever);
// 如果没有互联网连接,本地时间返回年份 70
if (year != 70) {
// 创建负载
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);
}
// 电池设置
// 注意:这些值仅作为示例,适用于我的 AGM 电池
std::vector<uint16_t> battery_settings1 = {
0, // 9000 电池类型 0 = 用户
0x0073, // 9001 电池容量 0x55 == 115AH
0x012C, // 9002 温度补偿 -3V /°C/2V
0x05DC, // 9003 0x5DC == 1500 过压断开电压 15.0
0x058C, // 9004 0x58C == 1480 充电限制电压 14.8
0x058C, // 9005 过压恢复电压 14.8
0x05BF, // 9006 均衡充电电压 14.6
0x05BE, // 9007 提升充电电压 14.7
0x0550, // 9008 浮充电压 13.6
0x0528, // 9009 提升恢复充电电压 13.2
0x04C4, // 900A 低压恢复电压 12.2
0x04B0, // 900B 欠压警告恢复电压 12.0
0x04BA, // 900c 欠压警告电压 12.1
0x04BA, // 900d 低压断开电压 11.8
0x04BA // 900E 放电限制电压 11.8
};
// 提升和均衡时间
std::vector<uint16_t> battery_settings2 = {
0x0000, // 906B 均衡时间(分钟) 0
0x0075 // 906C 提升时间(即吸收)117 分钟
};
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
## Modbus 设备地址
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_controlleroffline 变为 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!"