diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 12e2273bf88..e98a61257c6 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -116,7 +116,12 @@ from .const import ( UDP, ) from .modbus import ModbusHub, async_modbus_setup -from .validators import number_validator, scan_interval_validator, struct_validator +from .validators import ( + duplicate_entity_validator, + number_validator, + scan_interval_validator, + struct_validator, +) _LOGGER = logging.getLogger(__name__) @@ -327,6 +332,7 @@ CONFIG_SCHEMA = vol.Schema( DOMAIN: vol.All( cv.ensure_list, scan_interval_validator, + duplicate_entity_validator, [ vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA), ], diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index b59557e58d2..543618e11fd 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -9,9 +9,11 @@ from typing import Any import voluptuous as vol from homeassistant.const import ( + CONF_ADDRESS, CONF_COUNT, CONF_NAME, CONF_SCAN_INTERVAL, + CONF_SLAVE, CONF_STRUCTURE, CONF_TIMEOUT, ) @@ -189,3 +191,35 @@ def scan_interval_validator(config: dict) -> dict: ) hub[CONF_TIMEOUT] = minimum_scan_interval - 1 return config + + +def duplicate_entity_validator(config: dict) -> dict: + """Control scan_interval.""" + for hub_index, hub in enumerate(config): + addresses: set[str] = set() + for component, conf_key in PLATFORMS: + if conf_key not in hub: + continue + names: set[str] = set() + errors: list[int] = [] + for index, entry in enumerate(hub[conf_key]): + name = entry[CONF_NAME] + addr = str(entry[CONF_ADDRESS]) + if CONF_SLAVE in entry: + addr += "_" + str(entry[CONF_SLAVE]) + if addr in addresses: + err = f"Modbus {component}/{name} address {addr} is duplicate, second entry not loaded!" + _LOGGER.warning(err) + errors.append(index) + elif name in names: + err = f"Modbus {component}/{name}  is duplicate, second entry not loaded!" + _LOGGER.warning(err) + errors.append(index) + else: + names.add(name) + addresses.add(addr) + + for i in reversed(errors): + del config[hub_index][conf_key][i] + + return config diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py index 266193294c6..a315d8176ae 100644 --- a/tests/components/modbus/test_cover.py +++ b/tests/components/modbus/test_cover.py @@ -235,7 +235,7 @@ async def test_restore_state_cover(hass, mock_test_state, mock_modbus): { CONF_NAME: f"{TEST_ENTITY_NAME}2", CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_ADDRESS: 1234, + CONF_ADDRESS: 1235, CONF_SCAN_INTERVAL: 0, }, ] diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py index fb65f737d27..821a5cace99 100644 --- a/tests/components/modbus/test_fan.py +++ b/tests/components/modbus/test_fan.py @@ -231,7 +231,7 @@ async def test_fan_service_turn(hass, caplog, mock_pymodbus): }, { CONF_NAME: f"{TEST_ENTITY_NAME}2", - CONF_ADDRESS: 17, + CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py index f679883e908..486dfdc64f8 100644 --- a/tests/components/modbus/test_light.py +++ b/tests/components/modbus/test_light.py @@ -231,7 +231,7 @@ async def test_light_service_turn(hass, caplog, mock_pymodbus): }, { CONF_NAME: f"{TEST_ENTITY_NAME}2", - CONF_ADDRESS: 17, + CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {}, diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py index 302189001c5..fb929d26caf 100644 --- a/tests/components/modbus/test_switch.py +++ b/tests/components/modbus/test_switch.py @@ -245,7 +245,7 @@ async def test_switch_service_turn(hass, caplog, mock_pymodbus): }, { CONF_NAME: f"{TEST_ENTITY_NAME}2", - CONF_ADDRESS: 17, + CONF_ADDRESS: 18, CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_SCAN_INTERVAL: 0, CONF_VERIFY: {},