mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
"""Tests for modbus_server configuration validation."""
|
|
|
|
import pytest
|
|
|
|
from esphome import config_validation as cv
|
|
from esphome.components.modbus_server import (
|
|
SERVER_SENSOR_VALUE_TYPE,
|
|
_validate_no_overlapping_registers,
|
|
_validate_register_ranges,
|
|
_validate_unique_bit_addresses,
|
|
)
|
|
from esphome.components.modbus_server.const import (
|
|
CONF_BITS,
|
|
CONF_REGISTERS,
|
|
CONF_VALUE_TYPE,
|
|
)
|
|
from esphome.const import CONF_ADDRESS
|
|
|
|
|
|
def _config(registers: list[tuple[int, str]]) -> dict:
|
|
return {
|
|
CONF_REGISTERS: [
|
|
{CONF_ADDRESS: address, CONF_VALUE_TYPE: value_type}
|
|
for address, value_type in registers
|
|
]
|
|
}
|
|
|
|
|
|
def _bits_config(addresses: list[int]) -> dict:
|
|
return {CONF_BITS: [{CONF_ADDRESS: address} for address in addresses]}
|
|
|
|
|
|
def test_non_overlapping_registers_pass() -> None:
|
|
# Values that tile the address space without gaps or overlaps are accepted.
|
|
config = _config([(0x00, "U_WORD"), (0x01, "U_DWORD"), (0x03, "U_WORD")])
|
|
assert _validate_no_overlapping_registers(config) is config
|
|
|
|
|
|
def test_registers_with_gaps_pass() -> None:
|
|
config = _config([(0x00, "U_WORD"), (0x05, "U_QWORD"), (0x20, "U_WORD")])
|
|
assert _validate_no_overlapping_registers(config) is config
|
|
|
|
|
|
def test_no_registers_pass() -> None:
|
|
assert _validate_no_overlapping_registers({}) == {}
|
|
|
|
|
|
def test_duplicate_address_rejected() -> None:
|
|
config = _config([(0x10, "U_WORD"), (0x10, "U_WORD")])
|
|
with pytest.raises(cv.Invalid, match="overlaps"):
|
|
_validate_no_overlapping_registers(config)
|
|
|
|
|
|
def test_unique_bit_addresses_pass() -> None:
|
|
config = _bits_config([0x00, 0x01, 0x02])
|
|
assert _validate_unique_bit_addresses(config) is config
|
|
|
|
|
|
def test_duplicate_bit_address_rejected() -> None:
|
|
# Coils and discrete inputs share one bit address space, so a repeated address is rejected.
|
|
config = _bits_config([0x05, 0x05])
|
|
with pytest.raises(cv.Invalid, match="more than once"):
|
|
_validate_unique_bit_addresses(config)
|
|
|
|
|
|
def test_multi_register_value_overlapping_neighbour_rejected() -> None:
|
|
# U_DWORD at 0x10 occupies 0x10 and 0x11; a U_WORD at 0x11 collides with its low word.
|
|
config = _config([(0x10, "U_DWORD"), (0x11, "U_WORD")])
|
|
with pytest.raises(cv.Invalid, match="overlaps"):
|
|
_validate_no_overlapping_registers(config)
|
|
|
|
|
|
def test_overlap_detected_regardless_of_order() -> None:
|
|
# The U_DWORD at 0x10 covers 0x10-0x11 and overlaps the U_WORD at 0x11 even when declared after it.
|
|
config = _config([(0x11, "U_WORD"), (0x10, "U_DWORD")])
|
|
with pytest.raises(cv.Invalid, match="overlaps"):
|
|
_validate_no_overlapping_registers(config)
|
|
|
|
|
|
def test_register_span_within_address_space_pass() -> None:
|
|
# A value whose span ends exactly at 0xFFFF is fine (U_QWORD at 0xFFFC covers 0xFFFC-0xFFFF).
|
|
config = _config([(0xFFFF, "U_WORD"), (0xFFFC, "U_QWORD")])
|
|
assert _validate_register_ranges(config) is config
|
|
|
|
|
|
def test_register_span_past_end_rejected() -> None:
|
|
# U_QWORD at 0xFFFE would need 0xFFFE-0x10001, running off the 16-bit address space.
|
|
config = _config([(0xFFFE, "U_QWORD")])
|
|
with pytest.raises(cv.Invalid, match="past the end"):
|
|
_validate_register_ranges(config)
|
|
|
|
|
|
def test_multi_register_value_at_last_address_rejected() -> None:
|
|
# A U_DWORD at 0xFFFF needs a second register at 0x10000, which does not exist.
|
|
config = _config([(0xFFFF, "U_DWORD")])
|
|
with pytest.raises(cv.Invalid, match="past the end"):
|
|
_validate_register_ranges(config)
|
|
|
|
|
|
def test_raw_value_type_rejected() -> None:
|
|
# RAW has no numeric encoding, so it is not offered as a server register type.
|
|
validator = cv.enum(SERVER_SENSOR_VALUE_TYPE)
|
|
with pytest.raises(cv.Invalid):
|
|
validator("RAW")
|
|
assert validator("U_WORD") == "U_WORD"
|
|
assert validator("U_WORD_S") == "U_WORD_S"
|
|
assert validator("S_WORD_S") == "S_WORD_S"
|