esphome: name: uart-mock-modbus-loopback 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 # Shared loopback fixture (see the shared_yaml markers in the test file); # register spaces are disjoint so each test only observes its own entities. uart_mock: - id: virtual_uart_server baud_rate: 9600 auto_start: true debug: on_tx: - then: - uart_mock.inject_rx: id: virtual_uart_controller data: !lambda return data; - id: virtual_uart_controller baud_rate: 9600 auto_start: true debug: on_tx: - then: - uart_mock.inject_rx: id: virtual_uart_server data: !lambda return data; globals: - id: reg10 type: uint16_t initial_value: "100" - id: reg11 type: uint16_t initial_value: "200" - id: reg12 type: uint16_t initial_value: "300" - id: reg13 type: uint16_t initial_value: "0xABCD" - id: reg30 type: uint16_t initial_value: "0" - id: reg40 type: uint16_t initial_value: "5" - id: reg50 type: uint16_t initial_value: "0" modbus: - uart_id: virtual_uart_server id: virtual_modbus_server role: server - uart_id: virtual_uart_controller id: virtual_modbus_controller role: client turnaround_time: 10ms modbus_controller: - address: 1 modbus_id: virtual_modbus_controller id: modbus_controller_1 update_interval: 1s modbus_server: - address: 1 modbus_id: virtual_modbus_server registers: - address: 0x01 value_type: U_WORD read_lambda: return 259; - address: 0x10 value_type: U_WORD read_lambda: return id(reg10); write_lambda: id(reg10) = x; return true; - address: 0x11 value_type: U_WORD read_lambda: return id(reg11); write_lambda: id(reg11) = x; return true; - address: 0x12 value_type: U_WORD read_lambda: return id(reg12); write_lambda: id(reg12) = x; return true; - address: 0x13 value_type: U_WORD read_lambda: return id(reg13); - address: 0x30 value_type: U_WORD read_lambda: return id(reg30); write_lambda: id(reg30) = x; return true; - address: 0x40 value_type: U_WORD read_lambda: return id(reg40); write_lambda: id(reg40) = x; return true; - address: 0x50 value_type: U_WORD read_lambda: return id(reg50); write_lambda: id(reg50) = x; return true; # Byte-based offset: 2 bytes -> register 0x11 (the old code folded it in as a # register count, hitting 0x12). assumed_state keeps the switch write-only. switch: - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "offset_switch" register_type: holding address: 0x10 offset: 2 assumed_state: true # Reading switch, byte offset 6 -> register 0x13; the pre-fix resolution (0x16) # would draw ILLEGAL_DATA_ADDRESS and never publish. - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "read_offset_switch" register_type: holding address: 0x10 offset: 6 bitmask: 0x1 # Coil switch whose write_lambda dispatches a holding-register write via `item`; # returning an empty optional suppresses the default coil write. - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "cross_switch" register_type: coil address: 0x00 assumed_state: true write_lambda: |- item->write_single_register(0x30, x ? 1234 : 0); return {}; # Active-low: the write_lambda inverts the wire value but the entity must still # report the requested state (assumed_state keeps the register unpolled). - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "invert_switch" register_type: holding address: 0x40 assumed_state: true write_lambda: |- return !x; # Uses the deprecated buffer parameter (legacy raw frame as words); the write # must land and the deprecation warning must fire only once per entity. number: - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "buf_number" id: buf_number address: 0x50 register_type: holding value_type: U_WORD min_value: 0 max_value: 1000 step: 1 write_lambda: |- // Legacy raw frame as words: [addr 0x01 | fc 0x06], register 0x0050, value. payload.push_back(0x0106); payload.push_back(0x0050); payload.push_back((uint16_t) x); return {}; sensor: - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "plain_read" address: 0x01 register_type: holding value_type: U_WORD # Custom PDU: read holding register 0x0001; device address and CRC are added # by the hub. The lambda parses the big-endian register value. - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "custom_read" custom_pdu: [0x03, 0x00, 0x01, 0x00, 0x01] lambda: |- if (data.size() < 2) return {}; return (float) ((data[0] << 8) | data[1]); - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "reg_10" address: 0x10 register_type: holding value_type: U_WORD - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "reg_11" address: 0x11 register_type: holding value_type: U_WORD - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "reg_12" address: 0x12 register_type: holding value_type: U_WORD - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "reg_30" address: 0x30 register_type: holding value_type: U_WORD - platform: modbus_controller modbus_controller_id: modbus_controller_1 name: "reg_40" address: 0x40 register_type: holding value_type: U_WORD # Reports the server-side register so the test can observe that the deprecated buffer write landed. - platform: template name: "written_value" id: written_value update_interval: 0.5s lambda: "return id(reg50);" button: - platform: template name: "Start Scenario" id: start_scenario_btn # Nothing to start (mock is autostart); tests drive entities directly