mirror of
https://github.com/esphome/esphome.git
synced 2026-05-31 16:17:41 +08:00
[modbus] Add integration tests for server and server via controller (#14845)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
@@ -198,6 +198,13 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
|
|||||||
' - "-g" # Add debug symbols',
|
' - "-g" # Add debug symbols',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Replace external component path placeholder if present
|
||||||
|
if "EXTERNAL_COMPONENT_PATH" in content:
|
||||||
|
external_components_path = str(
|
||||||
|
Path(__file__).parent / "fixtures" / "external_components"
|
||||||
|
)
|
||||||
|
content = content.replace("EXTERNAL_COMPONENT_PATH", external_components_path)
|
||||||
|
|
||||||
return content
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ uart_mock:
|
|||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
rx_full_threshold: 120
|
rx_full_threshold: 120
|
||||||
rx_timeout: 2
|
rx_timeout: 2
|
||||||
|
# auto_start must be false to avoid races: the test presses the
|
||||||
|
# "Start Scenario" button only after subscribing to states.
|
||||||
auto_start: false
|
auto_start: false
|
||||||
debug:
|
debug:
|
||||||
responses:
|
responses:
|
||||||
@@ -46,7 +48,7 @@ modbus:
|
|||||||
modbus_controller:
|
modbus_controller:
|
||||||
- address: 1
|
- address: 1
|
||||||
id: modbus_controller_ok
|
id: modbus_controller_ok
|
||||||
max_cmd_retries: 0
|
max_cmd_retries: 2
|
||||||
update_interval: 1s
|
update_interval: 1s
|
||||||
- address: 2
|
- address: 2
|
||||||
id: modbus_controller_slow
|
id: modbus_controller_slow
|
||||||
@@ -89,4 +91,4 @@ button:
|
|||||||
name: "Start Scenario"
|
name: "Start Scenario"
|
||||||
id: start_scenario_btn
|
id: start_scenario_btn
|
||||||
on_press:
|
on_press:
|
||||||
- lambda: 'id(virtual_uart_dev).start_scenario();'
|
- lambda: "id(virtual_uart_dev).start_scenario();"
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ uart:
|
|||||||
uart_mock:
|
uart_mock:
|
||||||
- id: virtual_uart_dev
|
- id: virtual_uart_dev
|
||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
|
# auto_start must be false to avoid races: the test presses the
|
||||||
|
# "Start Scenario" button only after subscribing to states.
|
||||||
auto_start: false
|
auto_start: false
|
||||||
debug:
|
debug:
|
||||||
on_tx:
|
on_tx:
|
||||||
@@ -40,7 +42,8 @@ uart_mock:
|
|||||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
|
||||||
- uart_mock.inject_rx: # Second USB packet: rest of response (staged with 40ms latency)
|
- uart_mock.inject_rx: # Second USB packet: rest of response (staged with 40ms latency)
|
||||||
delay: 40ms
|
delay: 40ms
|
||||||
data: !lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
data:
|
||||||
|
!lambda return{0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
|
||||||
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC,
|
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x42,0x6F,0xCC,0xCD,0x43,0x7C,0xB8,0x10,0x3D,0x38,0x51,0xEC,
|
||||||
0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B};
|
0x43,0x81,0x1B,0xE7,0x3B,0x03,0x12,0x6F,0x50,0x1B};
|
||||||
|
|
||||||
@@ -61,4 +64,4 @@ button:
|
|||||||
name: "Start Scenario"
|
name: "Start Scenario"
|
||||||
id: start_scenario_btn
|
id: start_scenario_btn
|
||||||
on_press:
|
on_press:
|
||||||
- lambda: 'id(virtual_uart_dev).start_scenario();'
|
- lambda: "id(virtual_uart_dev).start_scenario();"
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
esphome:
|
||||||
|
name: uart-mock-modbus-server-test
|
||||||
|
|
||||||
|
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:
|
||||||
|
injections:
|
||||||
|
- delay: 100ms
|
||||||
|
inject_rx: [0x01, 0x03, 0x00, 0x03, 0x00, 0x01, 0x74, 0x0A] # Read holding register 3 on device 1 (basic_read)
|
||||||
|
- delay: 100ms
|
||||||
|
# Read holding register 7 on device 2
|
||||||
|
# Reply from device 2
|
||||||
|
# Read holding register 5 on device 1 (read_after_peer_response)
|
||||||
|
inject_rx:
|
||||||
|
[
|
||||||
|
0x02,
|
||||||
|
0x03,
|
||||||
|
0x00,
|
||||||
|
0x07,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x35,
|
||||||
|
0xF8,
|
||||||
|
0x02,
|
||||||
|
0x03,
|
||||||
|
0x02,
|
||||||
|
0x00,
|
||||||
|
0xF0,
|
||||||
|
0xFC,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x03,
|
||||||
|
0x00,
|
||||||
|
0x05,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x94,
|
||||||
|
0x0B,
|
||||||
|
]
|
||||||
|
- delay: 100ms
|
||||||
|
inject_rx: [0x02, 0x03, 0x00, 0x07, 0x00, 0x01, 0x35, 0xF8] # Read holding register 7 on device 2, with no response
|
||||||
|
- delay: 100ms
|
||||||
|
# Read holding register 7 on device 2, with no response
|
||||||
|
# Read holding register A on device 1 (read_after_peer_timeout)
|
||||||
|
inject_rx:
|
||||||
|
[
|
||||||
|
0x02,
|
||||||
|
0x03,
|
||||||
|
0x00,
|
||||||
|
0x07,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0x35,
|
||||||
|
0xF8,
|
||||||
|
0x01,
|
||||||
|
0x03,
|
||||||
|
0x00,
|
||||||
|
0x0A,
|
||||||
|
0x00,
|
||||||
|
0x01,
|
||||||
|
0xA4,
|
||||||
|
0x08,
|
||||||
|
]
|
||||||
|
|
||||||
|
modbus:
|
||||||
|
uart_id: virtual_uart_dev
|
||||||
|
role: server
|
||||||
|
|
||||||
|
modbus_controller:
|
||||||
|
- address: 1
|
||||||
|
server_registers:
|
||||||
|
- address: 0x03
|
||||||
|
value_type: U_WORD
|
||||||
|
read_lambda: |-
|
||||||
|
id(basic_read).publish_state(1);
|
||||||
|
return 1;
|
||||||
|
- address: 0x05
|
||||||
|
value_type: U_WORD
|
||||||
|
read_lambda: |-
|
||||||
|
id(read_after_peer_response).publish_state(1);
|
||||||
|
return 1;
|
||||||
|
- address: 0x0A
|
||||||
|
value_type: U_WORD
|
||||||
|
read_lambda: |-
|
||||||
|
id(read_after_peer_timeout).publish_state(1);
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: template
|
||||||
|
name: "basic_read"
|
||||||
|
id: basic_read
|
||||||
|
- platform: template
|
||||||
|
name: "read_after_peer_response"
|
||||||
|
id: read_after_peer_response
|
||||||
|
- platform: template
|
||||||
|
name: "read_after_peer_timeout"
|
||||||
|
id: read_after_peer_timeout
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Start Scenario"
|
||||||
|
id: start_scenario_btn
|
||||||
|
on_press:
|
||||||
|
- lambda: "id(virtual_uart_dev).start_scenario();"
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
esphome:
|
||||||
|
name: uart-mock-modbus-server-contro
|
||||||
|
|
||||||
|
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_server
|
||||||
|
baud_rate: 9600
|
||||||
|
# auto_start must be true for loopback fixtures: the modbus controller
|
||||||
|
# polls on its update_interval immediately at boot, so the uart_mock
|
||||||
|
# forwarding must already be active or early requests are lost and
|
||||||
|
# generate modbus warnings.
|
||||||
|
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 # See comment on virtual_uart_server above
|
||||||
|
debug:
|
||||||
|
on_tx:
|
||||||
|
- then:
|
||||||
|
- uart_mock.inject_rx:
|
||||||
|
id: virtual_uart_server
|
||||||
|
data: !lambda return data;
|
||||||
|
|
||||||
|
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
|
||||||
|
update_interval: 1s
|
||||||
|
id: modbus_controller_1
|
||||||
|
|
||||||
|
- address: 1
|
||||||
|
modbus_id: virtual_modbus_server
|
||||||
|
id: modbus_server_1
|
||||||
|
server_registers:
|
||||||
|
- address: 0x01
|
||||||
|
value_type: U_WORD
|
||||||
|
read_lambda: return 99;
|
||||||
|
- address: 0x03
|
||||||
|
value_type: S_WORD
|
||||||
|
read_lambda: return -99;
|
||||||
|
- address: 0x05
|
||||||
|
value_type: U_DWORD
|
||||||
|
read_lambda: return 16909060;
|
||||||
|
- address: 0x08
|
||||||
|
value_type: S_DWORD
|
||||||
|
read_lambda: return -16909060;
|
||||||
|
- address: 0x0B
|
||||||
|
value_type: U_DWORD_R
|
||||||
|
read_lambda: return 67305985;
|
||||||
|
- address: 0x0E
|
||||||
|
value_type: S_DWORD_R
|
||||||
|
read_lambda: return -67305985;
|
||||||
|
- address: 0x11
|
||||||
|
value_type: U_QWORD
|
||||||
|
read_lambda: return 72623859790382856;
|
||||||
|
- address: 0x16
|
||||||
|
value_type: S_QWORD
|
||||||
|
read_lambda: return -72623859790382856;
|
||||||
|
- address: 0x1B
|
||||||
|
value_type: U_QWORD_R
|
||||||
|
read_lambda: return 578437695752307201;
|
||||||
|
- address: 0x20
|
||||||
|
value_type: S_QWORD_R
|
||||||
|
read_lambda: return -578437695752307201;
|
||||||
|
- address: 0x25
|
||||||
|
value_type: FP32
|
||||||
|
read_lambda: return 3.14;
|
||||||
|
- address: 0x28
|
||||||
|
value_type: FP32_R
|
||||||
|
read_lambda: return 3.14;
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_word"
|
||||||
|
address: 0x01
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_word"
|
||||||
|
address: 0x03
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_dword"
|
||||||
|
address: 0x05
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_dword"
|
||||||
|
address: 0x08
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_DWORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_dword_r"
|
||||||
|
address: 0x0B
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD_R
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_dword_r"
|
||||||
|
address: 0x0E
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_DWORD_R
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_qword"
|
||||||
|
address: 0x11
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_QWORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_qword"
|
||||||
|
address: 0x16
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_QWORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_qword_r"
|
||||||
|
address: 0x1B
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_QWORD_R
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_qword_r"
|
||||||
|
address: 0x20
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_QWORD_R
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_fp32"
|
||||||
|
address: 0x25
|
||||||
|
register_type: holding
|
||||||
|
value_type: FP32
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_fp32_r"
|
||||||
|
address: 0x28
|
||||||
|
register_type: holding
|
||||||
|
value_type: FP32_R
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Start Scenario"
|
||||||
|
id: start_scenario_btn
|
||||||
|
on_press:
|
||||||
|
- lambda: "id(virtual_uart_server).start_scenario();"
|
||||||
|
- lambda: "id(virtual_uart_controller).start_scenario();"
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
esphome:
|
||||||
|
name: uart-mock-modbus-server-mult
|
||||||
|
|
||||||
|
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_server
|
||||||
|
baud_rate: 9600
|
||||||
|
# auto_start must be true for loopback fixtures: the modbus controller
|
||||||
|
# polls on its update_interval immediately at boot, so the uart_mock
|
||||||
|
# forwarding must already be active or early requests are lost and
|
||||||
|
# generate modbus warnings.
|
||||||
|
auto_start: true
|
||||||
|
debug:
|
||||||
|
on_tx:
|
||||||
|
- then:
|
||||||
|
- uart_mock.inject_rx:
|
||||||
|
id: virtual_uart_controller
|
||||||
|
data: !lambda return data;
|
||||||
|
- uart_mock.inject_rx:
|
||||||
|
id: virtual_uart_server_2
|
||||||
|
data: !lambda return data;
|
||||||
|
- id: virtual_uart_server_2
|
||||||
|
baud_rate: 9600
|
||||||
|
auto_start: true # See comment on virtual_uart_server above
|
||||||
|
debug:
|
||||||
|
on_tx:
|
||||||
|
- then:
|
||||||
|
- uart_mock.inject_rx:
|
||||||
|
id: virtual_uart_server
|
||||||
|
data: !lambda return data;
|
||||||
|
- uart_mock.inject_rx:
|
||||||
|
id: virtual_uart_controller
|
||||||
|
data: !lambda return data;
|
||||||
|
- id: virtual_uart_controller
|
||||||
|
baud_rate: 9600
|
||||||
|
auto_start: true # See comment on virtual_uart_server above
|
||||||
|
debug:
|
||||||
|
on_tx:
|
||||||
|
- then:
|
||||||
|
- uart_mock.inject_rx:
|
||||||
|
id: virtual_uart_server
|
||||||
|
data: !lambda return data;
|
||||||
|
- uart_mock.inject_rx:
|
||||||
|
id: virtual_uart_server_2
|
||||||
|
data: !lambda return data;
|
||||||
|
|
||||||
|
modbus:
|
||||||
|
- uart_id: virtual_uart_server
|
||||||
|
id: virtual_modbus_server
|
||||||
|
role: server
|
||||||
|
- uart_id: virtual_uart_server_2
|
||||||
|
id: virtual_modbus_server_2
|
||||||
|
role: server
|
||||||
|
- uart_id: virtual_uart_controller
|
||||||
|
id: virtual_modbus_client
|
||||||
|
role: client
|
||||||
|
turnaround_time: 10ms
|
||||||
|
|
||||||
|
modbus_controller:
|
||||||
|
- address: 1
|
||||||
|
modbus_id: virtual_modbus_client
|
||||||
|
update_interval: 1s
|
||||||
|
id: modbus_controller_1
|
||||||
|
- address: 2
|
||||||
|
modbus_id: virtual_modbus_client
|
||||||
|
update_interval: 1s
|
||||||
|
id: modbus_controller_2
|
||||||
|
|
||||||
|
- address: 1
|
||||||
|
modbus_id: virtual_modbus_server
|
||||||
|
server_registers:
|
||||||
|
- address: 0x01
|
||||||
|
value_type: U_WORD
|
||||||
|
read_lambda: return 919;
|
||||||
|
- address: 2
|
||||||
|
modbus_id: virtual_modbus_server_2
|
||||||
|
server_registers:
|
||||||
|
- address: 0x01
|
||||||
|
value_type: U_WORD
|
||||||
|
read_lambda: return 929;
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_word"
|
||||||
|
address: 0x01
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_2
|
||||||
|
name: "reg_u_word_2"
|
||||||
|
address: 0x01
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Start Scenario"
|
||||||
|
id: start_scenario_btn
|
||||||
|
on_press:
|
||||||
|
- lambda: "id(virtual_uart_server).start_scenario();"
|
||||||
|
- lambda: "id(virtual_uart_server_2).start_scenario();"
|
||||||
|
- lambda: "id(virtual_uart_controller).start_scenario();"
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
esphome:
|
||||||
|
name: uart-mock-modbus-srv-write
|
||||||
|
|
||||||
|
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_server
|
||||||
|
baud_rate: 9600
|
||||||
|
# auto_start must be true for loopback fixtures: the modbus controller
|
||||||
|
# polls on its update_interval immediately at boot, so the uart_mock
|
||||||
|
# forwarding must already be active or early requests are lost and
|
||||||
|
# generate modbus warnings.
|
||||||
|
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 # See comment on virtual_uart_server above
|
||||||
|
debug:
|
||||||
|
on_tx:
|
||||||
|
- then:
|
||||||
|
- uart_mock.inject_rx:
|
||||||
|
id: virtual_uart_server
|
||||||
|
data: !lambda return data;
|
||||||
|
|
||||||
|
globals:
|
||||||
|
- id: stored_u_word
|
||||||
|
type: uint16_t
|
||||||
|
initial_value: "11"
|
||||||
|
- id: stored_s_word
|
||||||
|
type: int16_t
|
||||||
|
initial_value: "-11"
|
||||||
|
- id: stored_u_dword
|
||||||
|
type: uint32_t
|
||||||
|
initial_value: "1001"
|
||||||
|
- id: stored_s_dword
|
||||||
|
type: int32_t
|
||||||
|
initial_value: "-1001"
|
||||||
|
- id: stored_u_dword_r
|
||||||
|
type: uint32_t
|
||||||
|
initial_value: "3003"
|
||||||
|
- id: stored_s_dword_r
|
||||||
|
type: int32_t
|
||||||
|
initial_value: "-3003"
|
||||||
|
- id: stored_u_qword
|
||||||
|
type: uint64_t
|
||||||
|
initial_value: "5005"
|
||||||
|
- id: stored_s_qword
|
||||||
|
type: int64_t
|
||||||
|
initial_value: "-5005"
|
||||||
|
- id: stored_u_qword_r
|
||||||
|
type: uint64_t
|
||||||
|
initial_value: "7007"
|
||||||
|
- id: stored_s_qword_r
|
||||||
|
type: int64_t
|
||||||
|
initial_value: "-7007"
|
||||||
|
- id: stored_fp32
|
||||||
|
type: float
|
||||||
|
initial_value: "1.5"
|
||||||
|
- id: stored_fp32_r
|
||||||
|
type: float
|
||||||
|
initial_value: "2.5"
|
||||||
|
|
||||||
|
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
|
||||||
|
update_interval: 2s
|
||||||
|
id: modbus_controller_1
|
||||||
|
|
||||||
|
- address: 1
|
||||||
|
modbus_id: virtual_modbus_server
|
||||||
|
id: modbus_server_1
|
||||||
|
server_registers:
|
||||||
|
- address: 0x01
|
||||||
|
value_type: U_WORD
|
||||||
|
read_lambda: return id(stored_u_word);
|
||||||
|
write_lambda: id(stored_u_word) = x; return true;
|
||||||
|
- address: 0x03
|
||||||
|
value_type: S_WORD
|
||||||
|
read_lambda: return id(stored_s_word);
|
||||||
|
write_lambda: id(stored_s_word) = x; return true;
|
||||||
|
- address: 0x05
|
||||||
|
value_type: U_DWORD
|
||||||
|
read_lambda: return id(stored_u_dword);
|
||||||
|
write_lambda: id(stored_u_dword) = x; return true;
|
||||||
|
- address: 0x08
|
||||||
|
value_type: S_DWORD
|
||||||
|
read_lambda: return id(stored_s_dword);
|
||||||
|
write_lambda: id(stored_s_dword) = x; return true;
|
||||||
|
- address: 0x0B
|
||||||
|
value_type: U_DWORD_R
|
||||||
|
read_lambda: return id(stored_u_dword_r);
|
||||||
|
write_lambda: id(stored_u_dword_r) = x; return true;
|
||||||
|
- address: 0x0E
|
||||||
|
value_type: S_DWORD_R
|
||||||
|
read_lambda: return id(stored_s_dword_r);
|
||||||
|
write_lambda: id(stored_s_dword_r) = x; return true;
|
||||||
|
- address: 0x11
|
||||||
|
value_type: U_QWORD
|
||||||
|
read_lambda: return id(stored_u_qword);
|
||||||
|
write_lambda: id(stored_u_qword) = x; return true;
|
||||||
|
- address: 0x16
|
||||||
|
value_type: S_QWORD
|
||||||
|
read_lambda: return id(stored_s_qword);
|
||||||
|
write_lambda: id(stored_s_qword) = x; return true;
|
||||||
|
- address: 0x1B
|
||||||
|
value_type: U_QWORD_R
|
||||||
|
read_lambda: return id(stored_u_qword_r);
|
||||||
|
write_lambda: id(stored_u_qword_r) = x; return true;
|
||||||
|
- address: 0x20
|
||||||
|
value_type: S_QWORD_R
|
||||||
|
read_lambda: return id(stored_s_qword_r);
|
||||||
|
write_lambda: id(stored_s_qword_r) = x; return true;
|
||||||
|
- address: 0x25
|
||||||
|
value_type: FP32
|
||||||
|
read_lambda: return id(stored_fp32);
|
||||||
|
write_lambda: id(stored_fp32) = x; return true;
|
||||||
|
- address: 0x28
|
||||||
|
value_type: FP32_R
|
||||||
|
read_lambda: return id(stored_fp32_r);
|
||||||
|
write_lambda: id(stored_fp32_r) = x; return true;
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_word"
|
||||||
|
address: 0x01
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_word"
|
||||||
|
address: 0x03
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_WORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_dword"
|
||||||
|
address: 0x05
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_dword"
|
||||||
|
address: 0x08
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_DWORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_dword_r"
|
||||||
|
address: 0x0B
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD_R
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_dword_r"
|
||||||
|
address: 0x0E
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_DWORD_R
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_qword"
|
||||||
|
address: 0x11
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_QWORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_qword"
|
||||||
|
address: 0x16
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_QWORD
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_u_qword_r"
|
||||||
|
address: 0x1B
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_QWORD_R
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_s_qword_r"
|
||||||
|
address: 0x20
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_QWORD_R
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_fp32"
|
||||||
|
address: 0x25
|
||||||
|
register_type: holding
|
||||||
|
value_type: FP32
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "reg_fp32_r"
|
||||||
|
address: 0x28
|
||||||
|
register_type: holding
|
||||||
|
value_type: FP32_R
|
||||||
|
|
||||||
|
number:
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_u_word"
|
||||||
|
address: 0x01
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_WORD
|
||||||
|
min_value: 0
|
||||||
|
max_value: 65535
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_s_word"
|
||||||
|
address: 0x03
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_WORD
|
||||||
|
min_value: -16777215
|
||||||
|
max_value: 16777215
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_u_dword"
|
||||||
|
address: 0x05
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD
|
||||||
|
min_value: 0
|
||||||
|
max_value: 16777215
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_s_dword"
|
||||||
|
address: 0x08
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_DWORD
|
||||||
|
min_value: -16777215
|
||||||
|
max_value: 16777215
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_u_dword_r"
|
||||||
|
address: 0x0B
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_DWORD_R
|
||||||
|
min_value: 0
|
||||||
|
max_value: 16777215
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_s_dword_r"
|
||||||
|
address: 0x0E
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_DWORD_R
|
||||||
|
min_value: -16777215
|
||||||
|
max_value: 16777215
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_u_qword"
|
||||||
|
address: 0x11
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_QWORD
|
||||||
|
min_value: 0
|
||||||
|
max_value: 16777215
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_s_qword"
|
||||||
|
address: 0x16
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_QWORD
|
||||||
|
min_value: -16777215
|
||||||
|
max_value: 16777215
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_u_qword_r"
|
||||||
|
address: 0x1B
|
||||||
|
register_type: holding
|
||||||
|
value_type: U_QWORD_R
|
||||||
|
min_value: 0
|
||||||
|
max_value: 16777215
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_s_qword_r"
|
||||||
|
address: 0x20
|
||||||
|
register_type: holding
|
||||||
|
value_type: S_QWORD_R
|
||||||
|
min_value: -16777215
|
||||||
|
max_value: 16777215
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_fp32"
|
||||||
|
address: 0x25
|
||||||
|
register_type: holding
|
||||||
|
value_type: FP32
|
||||||
|
min_value: -16777215
|
||||||
|
max_value: 16777215
|
||||||
|
step: 0.01
|
||||||
|
- platform: modbus_controller
|
||||||
|
modbus_controller_id: modbus_controller_1
|
||||||
|
name: "write_fp32_r"
|
||||||
|
address: 0x28
|
||||||
|
register_type: holding
|
||||||
|
value_type: FP32_R
|
||||||
|
min_value: -16777215
|
||||||
|
max_value: 16777215
|
||||||
|
step: 0.01
|
||||||
|
|
||||||
|
button:
|
||||||
|
- platform: template
|
||||||
|
name: "Start Scenario"
|
||||||
|
id: start_scenario_btn
|
||||||
|
on_press:
|
||||||
|
- lambda: "id(virtual_uart_server).start_scenario();"
|
||||||
|
- lambda: "id(virtual_uart_controller).start_scenario();"
|
||||||
@@ -22,6 +22,8 @@ uart_mock:
|
|||||||
baud_rate: 9600
|
baud_rate: 9600
|
||||||
rx_full_threshold: 120
|
rx_full_threshold: 120
|
||||||
rx_timeout: 2
|
rx_timeout: 2
|
||||||
|
# auto_start must be false to avoid races: the test presses the
|
||||||
|
# "Start Scenario" button only after subscribing to states.
|
||||||
auto_start: false
|
auto_start: false
|
||||||
debug:
|
debug:
|
||||||
on_tx:
|
on_tx:
|
||||||
@@ -61,4 +63,4 @@ button:
|
|||||||
name: "Start Scenario"
|
name: "Start Scenario"
|
||||||
id: start_scenario_btn
|
id: start_scenario_btn
|
||||||
on_press:
|
on_press:
|
||||||
- lambda: 'id(virtual_uart_dev).start_scenario();'
|
- lambda: "id(virtual_uart_dev).start_scenario();"
|
||||||
|
|||||||
@@ -346,3 +346,109 @@ class SensorStateCollector:
|
|||||||
else:
|
else:
|
||||||
self._waiters.append((condition, future))
|
self._waiters.append((condition, future))
|
||||||
return future
|
return future
|
||||||
|
|
||||||
|
|
||||||
|
class SensorTracker:
|
||||||
|
"""Data-driven sensor state tracker with expected-value futures.
|
||||||
|
|
||||||
|
Tracks sensor state updates and resolves futures when sensors report
|
||||||
|
specific expected values. Eliminates per-sensor future boilerplate.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
tracker = SensorTracker(["reg_u_word", "reg_s_word"])
|
||||||
|
futures = tracker.expect_all({"reg_u_word": 99, "reg_s_word": -99})
|
||||||
|
# ... subscribe_states with tracker.on_state, start scenario ...
|
||||||
|
await tracker.await_all(futures)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, sensor_names: list[str]) -> None:
|
||||||
|
self.sensor_states: dict[str, list[float]] = {name: [] for name in sensor_names}
|
||||||
|
self.key_to_sensor: dict[int, str] = {}
|
||||||
|
self._expectations: dict[str, list[tuple[object, asyncio.Future]]] = {}
|
||||||
|
|
||||||
|
_ANY = object() # Sentinel: match any value
|
||||||
|
|
||||||
|
def expect(self, name: str, value: object) -> asyncio.Future:
|
||||||
|
"""Register an expected value for *name* and return a future for it."""
|
||||||
|
future: asyncio.Future = asyncio.get_running_loop().create_future()
|
||||||
|
self._expectations.setdefault(name, []).append((value, future))
|
||||||
|
return future
|
||||||
|
|
||||||
|
def expect_any(self, name: str) -> asyncio.Future:
|
||||||
|
"""Register a future that resolves on *any* state update for *name*."""
|
||||||
|
return self.expect(name, self._ANY)
|
||||||
|
|
||||||
|
def expect_all(self, expected: dict[str, object]) -> dict[str, asyncio.Future]:
|
||||||
|
"""Call ``expect`` for every entry and return a dict of futures."""
|
||||||
|
return {name: self.expect(name, value) for name, value in expected.items()}
|
||||||
|
|
||||||
|
def on_state(self, state: EntityState) -> None:
|
||||||
|
"""State callback suitable for ``subscribe_states``."""
|
||||||
|
if not isinstance(state, SensorState) or state.missing_state:
|
||||||
|
return
|
||||||
|
sensor_name = self.key_to_sensor.get(state.key)
|
||||||
|
if not sensor_name or sensor_name not in self.sensor_states:
|
||||||
|
return
|
||||||
|
self.sensor_states[sensor_name].append(state.state)
|
||||||
|
for expected_value, future in self._expectations.get(sensor_name, []):
|
||||||
|
if not future.done() and (
|
||||||
|
expected_value is self._ANY or state.state == expected_value
|
||||||
|
):
|
||||||
|
future.set_result(True)
|
||||||
|
break
|
||||||
|
|
||||||
|
async def await_change(
|
||||||
|
self, future: asyncio.Future, name: str, timeout: float = 2.0
|
||||||
|
) -> None:
|
||||||
|
"""Wait for a sensor future to resolve; fail the test on timeout."""
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(future, timeout=timeout)
|
||||||
|
except TimeoutError:
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.fail(
|
||||||
|
f"Timeout waiting for {name} change. Received sensor states:\n"
|
||||||
|
f" {name}: {self.sensor_states[name]}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def await_must_not_change(
|
||||||
|
self, future: asyncio.Future, name: str, timeout: float = 2.0
|
||||||
|
) -> None:
|
||||||
|
"""Assert a sensor future does NOT resolve within the timeout."""
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(future, timeout=timeout)
|
||||||
|
except TimeoutError:
|
||||||
|
return # Expected
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.fail(
|
||||||
|
f"{name} change should not have been triggered, but was. "
|
||||||
|
f"Received sensor states:\n {name}: {self.sensor_states[name]}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
async def await_all(
|
||||||
|
self, futures: dict[str, asyncio.Future], timeout: float = 2.0
|
||||||
|
) -> None:
|
||||||
|
"""Await every future in *futures*, failing with per-sensor diagnostics."""
|
||||||
|
for name, future in futures.items():
|
||||||
|
await self.await_change(future, name, timeout=timeout)
|
||||||
|
|
||||||
|
async def setup_and_start_scenario(self, client) -> list:
|
||||||
|
"""Wire up subscriptions, wait for initial states, press Start Scenario."""
|
||||||
|
entities, _ = await client.list_entities_services()
|
||||||
|
self.key_to_sensor.update(
|
||||||
|
build_key_to_entity_mapping(entities, list(self.sensor_states.keys()))
|
||||||
|
)
|
||||||
|
initial_state_helper = InitialStateHelper(entities)
|
||||||
|
client.subscribe_states(initial_state_helper.on_state_wrapper(self.on_state))
|
||||||
|
try:
|
||||||
|
await initial_state_helper.wait_for_initial_states()
|
||||||
|
except TimeoutError:
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
pytest.fail("Timeout waiting for initial states")
|
||||||
|
start_btn = find_entity(entities, "start_scenario", ButtonInfo)
|
||||||
|
assert start_btn is not None, "Start Scenario button not found"
|
||||||
|
client.button_command(start_btn.key)
|
||||||
|
return entities
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user