diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 734546a34bc..f81a1200905 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -137,6 +137,7 @@ from .validators import ( duplicate_fan_mode_validator, nan_validator, number_validator, + register_int_list_validator, struct_validator, ) @@ -279,7 +280,7 @@ CLIMATE_SCHEMA = vol.All( vol.Optional(CONF_FAN_MODE_REGISTER): vol.Maybe( vol.All( { - CONF_ADDRESS: cv.positive_int, + vol.Required(CONF_ADDRESS): register_int_list_validator, CONF_FAN_MODE_VALUES: { vol.Optional(CONF_FAN_MODE_ON): cv.positive_int, vol.Optional(CONF_FAN_MODE_OFF): cv.positive_int, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index aa345324dc8..71c01d20205 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -170,7 +170,6 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._fan_mode_mapping_to_modbus: dict[str, int] = {} self._fan_mode_mapping_from_modbus: dict[int, str] = {} mode_value_config = mode_config[CONF_FAN_MODE_VALUES] - for fan_mode_kw, fan_mode in ( (CONF_FAN_MODE_ON, FAN_ON), (CONF_FAN_MODE_OFF, FAN_OFF), @@ -253,16 +252,23 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" - if self._fan_mode_register is not None: # Write a value to the mode register for the desired mode. value = self._fan_mode_mapping_to_modbus[fan_mode] - await self._hub.async_pb_call( - self._slave, - self._fan_mode_register, - value, - CALL_TYPE_WRITE_REGISTER, - ) + if isinstance(self._fan_mode_register, list): + await self._hub.async_pb_call( + self._slave, + self._fan_mode_register[0], + [value], + CALL_TYPE_WRITE_REGISTERS, + ) + else: + await self._hub.async_pb_call( + self._slave, + self._fan_mode_register, + value, + CALL_TYPE_WRITE_REGISTER, + ) await self.async_update() @@ -344,7 +350,11 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): # Read the Fan mode register if defined if self._fan_mode_register is not None: fan_mode = await self._async_read_register( - CALL_TYPE_REGISTER_HOLDING, self._fan_mode_register, raw=True + CALL_TYPE_REGISTER_HOLDING, + self._fan_mode_register + if isinstance(self._fan_mode_register, int) + else self._fan_mode_register[0], + raw=True, ) # Translate the value received diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index e108231c5e6..e8ce35e834f 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -293,7 +293,11 @@ def duplicate_entity_validator(config: dict) -> dict: a += "_" + str(inx) entry_addrs.add(a) if CONF_FAN_MODE_REGISTER in entry: - a = str(entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]) + a = str( + entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS] + if isinstance(entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS], int) + else entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS][0] + ) a += "_" + str(inx) entry_addrs.add(a) @@ -351,6 +355,20 @@ def duplicate_modbus_validator(config: dict) -> dict: return config +def register_int_list_validator(value: Any) -> Any: + """Check if a register (CONF_ADRESS) is an int or a list having only 1 register.""" + if isinstance(value, int) and value >= 0: + return value + + if isinstance(value, list): + if (len(value) == 1) and isinstance(value[0], int) and value[0] >= 0: + return value + + raise vol.Invalid( + f"Invalid {CONF_ADDRESS} register for fan mode. Required type: positive integer, allowed 1 or list of 1 register." + ) + + def check_config(config: dict) -> dict: """Do final config check.""" config2 = duplicate_modbus_validator(config) diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index b6855d7be18..3ff9aa37bcf 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -30,6 +30,7 @@ from homeassistant.components.modbus.const import ( CONF_FAN_MODE_OFF, CONF_FAN_MODE_ON, CONF_FAN_MODE_REGISTER, + CONF_FAN_MODE_TOP, CONF_FAN_MODE_VALUES, CONF_HVAC_MODE_AUTO, CONF_HVAC_MODE_COOL, @@ -491,7 +492,7 @@ async def test_service_climate_update( CONF_SCAN_INTERVAL: 0, CONF_DATA_TYPE: DataType.INT32, CONF_FAN_MODE_REGISTER: { - CONF_ADDRESS: 118, + CONF_ADDRESS: [118], CONF_FAN_MODE_VALUES: { CONF_FAN_MODE_LOW: 0, CONF_FAN_MODE_MEDIUM: 1, @@ -505,6 +506,31 @@ async def test_service_climate_update( FAN_HIGH, [0x02], ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_FAN_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_FAN_MODE_VALUES: { + CONF_FAN_MODE_LOW: 0, + CONF_FAN_MODE_MEDIUM: 1, + CONF_FAN_MODE_HIGH: 2, + CONF_FAN_MODE_TOP: 3, + }, + }, + }, + ] + }, + FAN_TOP, + [0x03], + ), ], ) async def test_service_climate_fan_update( @@ -740,7 +766,7 @@ async def test_service_set_hvac_mode( CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_FAN_MODE_REGISTER: { - CONF_ADDRESS: 118, + CONF_ADDRESS: [118], CONF_FAN_MODE_VALUES: { CONF_FAN_MODE_ON: 1, CONF_FAN_MODE_OFF: 2, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index da46979526f..24ae8d0ebfc 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -84,6 +84,7 @@ from homeassistant.components.modbus.validators import ( duplicate_modbus_validator, nan_validator, number_validator, + register_int_list_validator, struct_validator, ) from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -138,6 +139,24 @@ async def mock_modbus_with_pymodbus_fixture(hass, caplog, do_config, mock_pymodb return mock_pymodbus +async def test_register_int_list_validator() -> None: + """Test conf address register validator.""" + for value, vtype in ( + (15, int), + ([15], list), + ): + assert isinstance(register_int_list_validator(value), vtype) + + with pytest.raises(vol.Invalid): + register_int_list_validator([15, 16]) + + with pytest.raises(vol.Invalid): + register_int_list_validator(-15) + + with pytest.raises(vol.Invalid): + register_int_list_validator(["aq"]) + + async def test_number_validator() -> None: """Test number validator.""" @@ -584,7 +603,7 @@ async def test_duplicate_entity_validator(do_config) -> None: CONF_SLAVE: 0, CONF_TARGET_TEMP: 117, CONF_FAN_MODE_REGISTER: { - CONF_ADDRESS: 121, + CONF_ADDRESS: [121], CONF_FAN_MODE_VALUES: { CONF_FAN_MODE_ON: 0, CONF_FAN_MODE_HIGH: 1,