esphome: name: uart-mock-modbus-shared host: api: logger: level: VERBOSE external_components: - source: type: local path: EXTERNAL_COMPONENT_PATH # Dummy uart entry to satisfy modbus's DEPENDENCIES = ["uart"] # The actual UART bus used is the uart_mock component below uart: baud_rate: 115200 port: /dev/null uart_mock: - id: virtual_uart_dev baud_rate: 9600 rx_full_threshold: 120 rx_timeout: 2 auto_start: false debug: responses: # Three sensors, one frame. At 0x9001 a U_WORD (1 register) and a U_DWORD (2 registers) share the # start address but cannot merge, so the range widens to count 2. A third sensor at 0x9002 falls # inside the widened range and must read its slice of the same response rather than splitting into # a second overlapping poll. The single expect_tx pins the "one frame on the wire" contract - any # duplicate or overlapping range would put an extra frame on the bus and fail to match. - expect_tx: [0x01, 0x03, 0x90, 0x01, 0x00, 0x02, 0xB8, 0xCB] # Read holding 0x9001 count 2 on device 1 inject_rx: [0x01, 0x03, 0x04, 0x03, 0x97, 0x02, 0x91, 0x8B, 0x57] # 0x9001=0x0397, 0x9002=0x0291 # A force_new_range sensor at a HIGH address (0x30) sorts before the plain sensor at a LOW address # (0x10). The two must poll as separate ranges: the covered branch's lower-bound check prevents the # 0x10 sensor from being absorbed into the forced 0x30 range with a wrapped byte offset. - expect_tx: [0x01, 0x03, 0x00, 0x30, 0x00, 0x01, 0x84, 0x05] # Read holding 0x30 count 1 (forced range) inject_rx: [0x01, 0x03, 0x02, 0x01, 0x11, 0x79, 0xD8] # 0x30 = 0x0111 = 273 - expect_tx: [0x01, 0x03, 0x00, 0x10, 0x00, 0x01, 0x85, 0xCF] # Read holding 0x10 count 1 (own range) inject_rx: [0x01, 0x03, 0x02, 0x02, 0x22, 0x39, 0x3D] # 0x10 = 0x0222 = 546 # A wide sensor (U_QWORD at 0x100, 4 registers) followed by plain sensors at 0x101 and 0x103. # None of them merge, so all three poll separately - exactly as before the range refactor. The # 0x103 sensor sits at the wide range's tail address, so it must not anchor a re-use join on a # mid-range predecessor and inherit its byte offset. - expect_tx: [0x01, 0x03, 0x01, 0x00, 0x00, 0x04, 0x45, 0xF5] # Read holding 0x100 count 4 inject_rx: [0x01, 0x03, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x94, 0x3C] # = 100 - expect_tx: [0x01, 0x03, 0x01, 0x01, 0x00, 0x01, 0xD4, 0x36] # Read holding 0x101 count 1 inject_rx: [0x01, 0x03, 0x02, 0x01, 0x41, 0x79, 0xE4] # 0x101 = 0x0141 = 321 - expect_tx: [0x01, 0x03, 0x01, 0x03, 0x00, 0x01, 0x75, 0xF6] # Read holding 0x103 count 1 inject_rx: [0x01, 0x03, 0x02, 0x01, 0xA5, 0x79, 0xAF] # 0x103 = 0x01A5 = 421 # A widened shared-address range at 0x200 plus a sensor at 0x201 carrying its own skip_updates. # The sensor must keep its own range so the polling rates stay independent; if it were folded into # the widened range it would decode 0x201 from THAT response (2, not 777) and drag the range's # rate down to its own. - expect_tx: [0x01, 0x03, 0x02, 0x00, 0x00, 0x02, 0xC5, 0xB3] # Read holding 0x200 count 2 inject_rx: [0x01, 0x03, 0x04, 0x01, 0x41, 0x00, 0x02, 0x2A, 0x1A] # 0x200=0x0141, 0x201=0x0002 - expect_tx: [0x01, 0x03, 0x02, 0x01, 0x00, 0x01, 0xD4, 0x72] # Read holding 0x201 count 1 inject_rx: [0x01, 0x03, 0x02, 0x03, 0x09, 0x78, 0xB2] # 0x201 = 0x0309 = 777 modbus: uart_id: virtual_uart_dev send_wait_time: 200ms turnaround_time: 10ms modbus_controller: - address: 1 id: modbus_controller_ok max_cmd_retries: 2 update_interval: never sensor: # Word sensor at 0x9001 (1 register) - platform: modbus_controller name: "shared_word" address: 0x9001 register_type: holding value_type: U_WORD modbus_controller_id: modbus_controller_ok # Dword sensor at the SAME address 0x9001 (2 registers) - non-mergeable, shares the range start - platform: modbus_controller name: "shared_dword" address: 0x9001 register_type: holding value_type: U_DWORD modbus_controller_id: modbus_controller_ok # Word sensor at 0x9002 - inside the widened range, reads bytes 2-3 of the same response - platform: modbus_controller name: "covered_word" address: 0x9002 register_type: holding value_type: U_WORD modbus_controller_id: modbus_controller_ok # Forced sensor at a high address: sorts first, opens its own isolated range - platform: modbus_controller name: "forced_high" address: 0x30 register_type: holding value_type: U_WORD force_new_range: true modbus_controller_id: modbus_controller_ok # Plain sensor at a lower address: must get its own range, never absorbed into the forced one - platform: modbus_controller name: "plain_low" address: 0x10 register_type: holding value_type: U_WORD modbus_controller_id: modbus_controller_ok # Wide sensor spanning 0x100-0x103; the two sensors below sit inside its span but do not merge - platform: modbus_controller name: "wide_qword" address: 0x100 register_type: holding value_type: U_QWORD modbus_controller_id: modbus_controller_ok - platform: modbus_controller name: "inside_wide" address: 0x101 register_type: holding value_type: U_WORD modbus_controller_id: modbus_controller_ok # At the wide range's tail address: must decode its own poll, not inherit a mid-range byte offset - platform: modbus_controller name: "tail_of_wide" address: 0x103 register_type: holding value_type: U_WORD modbus_controller_id: modbus_controller_ok # Shared address 0x200: the dword widens the range the word opened (or vice versa) - platform: modbus_controller name: "rate_word" address: 0x200 register_type: holding value_type: U_WORD modbus_controller_id: modbus_controller_ok - platform: modbus_controller name: "rate_dword" address: 0x200 register_type: holding value_type: U_DWORD modbus_controller_id: modbus_controller_ok # Inside the widened range but with its own skip_updates: must NOT be folded in, or the two sensors # above would silently drop to this sensor's polling rate - platform: modbus_controller name: "own_rate" address: 0x201 register_type: holding value_type: U_WORD skip_updates: 100 modbus_controller_id: modbus_controller_ok button: - platform: template name: "Start Scenario" id: start_scenario_btn on_press: - lambda: |- id(virtual_uart_dev).start_scenario(); id(modbus_controller_ok).set_update_interval(1000); id(modbus_controller_ok).start_poller();