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): 手动指定
modbushub 的 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。
自动化:
- on_command_sent (可选, Automation): 当 modbus 命令已被发送时执行的自动化。参见
on_command_sent - on_online (可选, Automation): 当 modbus 控制器上线时执行的自动化。参见
on_online - on_offline (可选, Automation): 当 modbus 控制器离线时执行的自动化。参见
on_offline
以下代码创建一个 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 | 二进制传感器 0 | 1 | 1 |
| bit 1 | 二进制传感器 1 | 2 | 2 |
| bit 2 | 二进制传感器 2 | 4 | 4 |
| bit 3 | 二进制传感器 3 | 8 | 8 |
| bit 4 | 二进制传感器 4 | 16 | 10 |
| bit 5 | 二进制传感器 5 | 32 | 20 |
| bit 6 | 二进制传感器 6 | 64 | 40 |
| bit 7 | 二进制传感器 7 | 128 | 80 |
| bit 8 | 二进制传感器 8 | 256 | 100 |
| bit 9 | 二进制传感器 9 | 512 | 200 |
| bit 10 | 二进制传感器 10 | 1024 | 400 |
| bit 11 | 二进制传感器 11 | 2048 | 800 |
| bit 12 | 二进制传感器 12 | 4096 | 1000 |
| bit 13 | 二进制传感器 13 | 8192 | 2000 |
| bit 14 | 二进制传感器 14 | 16384 | 4000 |
| bit 15 | 二进制传感器 15 | 32768 | 8000 |
在下面的示例中,寄存器 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
Section titled “使用 custom_command”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优化 modbus 通信
Section titled “优化 modbus 通信”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。
协议解码示例
Section titled “协议解码示例”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 |
响应:
| offset | data | value (type) | 描述 |
|---|---|---|---|
| H | 0x1 (01) | 设备地址 | |
| H | 0x4 (04) | 功能码 | |
| H | 0x12 (18) | 字节数 | |
| 0 | 0x27 (39) | U_WORD | array_rated_voltage 高字节 |
| 1 | 0x10 (16) | 0x2710 (100000) | array_rated_voltage 低字节 |
| 2 | 0x7 (7) | U_WORD | array_rated_current 高字节 |
| 3 | 0xd0 (208) | 0x7d0 (2000) | array_rated_current 低字节 |
| 4 | 0xcb (203) | U_DWORD_R | array_rated_power 低字的高字节 |
| 5 | 0x20 (32) | spans 2 register | array_rated_power 低字的低字节 |
| 6 | 0x0 (0) | array_rated_power 高字的高字节 | |
| 7 | 0x0 (0) | 0x0000CB20 (52000) | array_rated_power 高字的低字节 |
| 8 | 0x9 (09) | U_WORD | battery_rated_voltage 高字节 |
| 9 | 0x60 (96) | 0x960 (2400) | battery_rated_voltage 低字节 |
| 10 | 0x7 (07) | U_WORD | battery_rated_current 高字 |
| 11 | 0xd0 (208) | 0x7d0 (2000) | battery_rated_current 高字 |
| 12 | 0xcb (203) | U_DWORD_R | battery_rated_power 低字的高字节 |
| 13 | 0x20 (32) | spans 2 register | battery_rated_power 低字的低字节 |
| 14 | 0x0 (0) | battery_rated_power 高字的高字节 | |
| 15 | 0x0 (0) | 0x0000CB20 (52000) | battery_rated_power 高字的低字节 |
| 16 | 0x0 (0) | U_WORD | charging_mode 高字节 |
| 17 | 0x2 (02) | 0x2 (MPPT) | charging_mode 低字节 |
| C | 0x2f (47) | crc | |
| C | 0x31 (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.01on_command_sent
Section titled “on_command_sent”当 modbus_controller 发送了一条命令时,将触发此自动化。在 Lambdas
中,你可以在 function_code 中获取功能码,在 address 中获取寄存器地址。
modbus_controller: - id: modbus_con # ... on_command_sent: then: - number.increment: modbus_commandson_online
Section titled “on_online”当 modbus_controller 在经历 offline(离线)状态之后变为 online(在线)时,将触发此自动化。在 Lambdas
中,你可以在 function_code 中获取功能码,在 address 中获取寄存器地址。
modbus_controller: - id: modbus_con # ... on_online: then: - logger.log: "Controller back online!"on_offline
Section titled “on_offline”当 modbus_controller 变为 offline(离线)时,将触发此自动化(参见 offline_skip_updates)。在 Lambdas
中,你可以在 function_code 中获取功能码,在 address 中获取寄存器地址。
modbus_controller: - id: modbus_con # ... on_offline: then: - logger.log: "Controller goes offline!"