diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 94a84d3440d..2a82cf89fd5 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -113,6 +113,13 @@ from .const import ( CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_HORIZ, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_SWING_VERT, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_VERIFY, @@ -134,6 +141,7 @@ from .modbus import ModbusHub, async_modbus_setup from .validators import ( check_hvac_target_temp_registers, duplicate_fan_mode_validator, + duplicate_swing_mode_validator, hvac_fixedsize_reglist_validator, nan_validator, register_int_list_validator, @@ -296,6 +304,21 @@ CLIMATE_SCHEMA = vol.All( duplicate_fan_mode_validator, ), ), + vol.Optional(CONF_SWING_MODE_REGISTER): vol.Maybe( + vol.All( + { + vol.Required(CONF_ADDRESS): register_int_list_validator, + CONF_SWING_MODE_VALUES: { + vol.Optional(CONF_SWING_MODE_SWING_ON): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_OFF): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_HORIZ): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_VERT): cv.positive_int, + vol.Optional(CONF_SWING_MODE_SWING_BOTH): cv.positive_int, + }, + }, + duplicate_swing_mode_validator, + ) + ), }, ), check_hvac_target_temp_registers, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 07dd12d3c94..0a4eae341b4 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -3,6 +3,7 @@ from __future__ import annotations from datetime import datetime +import logging import struct from typing import Any, cast @@ -17,6 +18,11 @@ from homeassistant.components.climate import ( FAN_OFF, FAN_ON, FAN_TOP, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, ClimateEntity, ClimateEntityFeature, HVACMode, @@ -28,6 +34,7 @@ from homeassistant.const import ( CONF_TEMPERATURE_UNIT, PRECISION_TENTHS, PRECISION_WHOLE, + STATE_UNKNOWN, UnitOfTemperature, ) from homeassistant.core import HomeAssistant @@ -67,6 +74,13 @@ from .const import ( CONF_MAX_TEMP, CONF_MIN_TEMP, CONF_STEP, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_HORIZ, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_SWING_VERT, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, @@ -74,6 +88,8 @@ from .const import ( ) from .modbus import ModbusHub +_LOGGER = logging.getLogger(__name__) + PARALLEL_UPDATES = 1 HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY = { @@ -204,11 +220,35 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): self._attr_fan_modes.append(fan_mode) else: - # No HVAC modes defined + # No FAN modes defined self._fan_mode_register = None self._attr_fan_mode = FAN_AUTO self._attr_fan_modes = [FAN_AUTO] + # No SWING modes defined + self._swing_mode_register = None + if CONF_SWING_MODE_REGISTER in config: + self._attr_supported_features = ( + self._attr_supported_features | ClimateEntityFeature.SWING_MODE + ) + mode_config = config[CONF_SWING_MODE_REGISTER] + self._swing_mode_register = mode_config[CONF_ADDRESS] + self._attr_swing_modes = cast(list[str], []) + self._attr_swing_mode = None + self._swing_mode_modbus_mapping: list[tuple[int, str]] = [] + mode_value_config = mode_config[CONF_SWING_MODE_VALUES] + for swing_mode_kw, swing_mode in ( + (CONF_SWING_MODE_SWING_ON, SWING_ON), + (CONF_SWING_MODE_SWING_OFF, SWING_OFF), + (CONF_SWING_MODE_SWING_HORIZ, SWING_HORIZONTAL), + (CONF_SWING_MODE_SWING_VERT, SWING_VERTICAL), + (CONF_SWING_MODE_SWING_BOTH, SWING_BOTH), + ): + if swing_mode_kw in mode_value_config: + value = mode_value_config[swing_mode_kw] + self._swing_mode_modbus_mapping.append((value, swing_mode)) + self._attr_swing_modes.append(swing_mode) + if CONF_HVAC_ONOFF_REGISTER in config: self._hvac_onoff_register = config[CONF_HVAC_ONOFF_REGISTER] self._hvac_onoff_write_registers = config[CONF_WRITE_REGISTERS] @@ -287,6 +327,29 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): await self.async_update() + async def async_set_swing_mode(self, swing_mode: str) -> None: + """Set new target swing mode.""" + if self._swing_mode_register: + # Write a value to the mode register for the desired mode. + for value, smode in self._swing_mode_modbus_mapping: + if swing_mode == smode: + if isinstance(self._swing_mode_register, list): + await self._hub.async_pb_call( + self._slave, + self._swing_mode_register[0], + [value], + CALL_TYPE_WRITE_REGISTERS, + ) + else: + await self._hub.async_pb_call( + self._slave, + self._swing_mode_register, + value, + CALL_TYPE_WRITE_REGISTER, + ) + break + await self.async_update() + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" target_temperature = ( @@ -387,6 +450,26 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity): int(fan_mode), self._attr_fan_mode ) + # Read the Swing mode register if defined + if self._swing_mode_register: + swing_mode = await self._async_read_register( + CALL_TYPE_REGISTER_HOLDING, + self._swing_mode_register + if isinstance(self._swing_mode_register, int) + else self._swing_mode_register[0], + raw=True, + ) + + self._attr_swing_mode = STATE_UNKNOWN + for value, smode in self._swing_mode_modbus_mapping: + if swing_mode == value: + self._attr_swing_mode = smode + break + + if self._attr_swing_mode is STATE_UNKNOWN: + _err = f"{self.name}: No answer received from Swing mode register. State is Unknown" + _LOGGER.error(_err) + # Read the on/off register if defined. If the value in this # register is "OFF", it will take precedence over the value # in the mode register. diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index 425bd744a1e..02f5d99c72c 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -70,6 +70,13 @@ CONF_HVAC_MODE_AUTO = "state_auto" CONF_HVAC_MODE_DRY = "state_dry" CONF_HVAC_MODE_FAN_ONLY = "state_fan_only" CONF_HVAC_MODE_VALUES = "values" +CONF_SWING_MODE_REGISTER = "swing_mode_register" +CONF_SWING_MODE_SWING_BOTH = "swing_mode_state_both" +CONF_SWING_MODE_SWING_HORIZ = "swing_mode_state_horizontal" +CONF_SWING_MODE_SWING_OFF = "swing_mode_state_off" +CONF_SWING_MODE_SWING_ON = "swing_mode_state_on" +CONF_SWING_MODE_SWING_VERT = "swing_mode_state_vertical" +CONF_SWING_MODE_VALUES = "values" CONF_WRITE_REGISTERS = "write_registers" CONF_VERIFY = "verify" CONF_VIRTUAL_COUNT = "virtual_count" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 7de2ecbe604..5071d098db7 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -42,6 +42,8 @@ from .const import ( CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_VIRTUAL_COUNT, CONF_WRITE_TYPE, @@ -256,8 +258,25 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict: return config +def duplicate_swing_mode_validator(config: dict[str, Any]) -> dict: + """Control modbus climate swing mode values for duplicates.""" + swing_modes: set[int] = set() + errors = [] + for key, value in config[CONF_SWING_MODE_VALUES].items(): + if value in swing_modes: + warn = f"Modbus swing mode {key} has a duplicate value {value}, not loaded, values must be unique!" + _LOGGER.warning(warn) + errors.append(key) + else: + swing_modes.add(value) + + for key in reversed(errors): + del config[CONF_SWING_MODE_VALUES][key] + return config + + def check_hvac_target_temp_registers(config: dict) -> dict: - """Check conflicts among HVAC target temperature registers and HVAC ON/OFF, HVAC register, Fan Modes.""" + """Check conflicts among HVAC target temperature registers and HVAC ON/OFF, HVAC register, Fan Modes, Swing Modes.""" if ( CONF_HVAC_MODE_REGISTER in config @@ -281,6 +300,17 @@ def check_hvac_target_temp_registers(config: dict) -> dict: _LOGGER.warning(wrn) del config[CONF_FAN_MODE_REGISTER] + if CONF_SWING_MODE_REGISTER in config: + regToTest = ( + config[CONF_SWING_MODE_REGISTER][CONF_ADDRESS] + if isinstance(config[CONF_SWING_MODE_REGISTER][CONF_ADDRESS], int) + else config[CONF_SWING_MODE_REGISTER][CONF_ADDRESS][0] + ) + if regToTest in config[CONF_TARGET_TEMP]: + wrn = f"{CONF_SWING_MODE_REGISTER} overlaps CONF_TARGET_TEMP register(s). {CONF_SWING_MODE_REGISTER} is not loaded!" + _LOGGER.warning(wrn) + del config[CONF_SWING_MODE_REGISTER] + return config @@ -294,7 +324,7 @@ def register_int_list_validator(value: Any) -> Any: return value raise vol.Invalid( - f"Invalid {CONF_ADDRESS} register for fan mode. Required type: positive integer, allowed 1 or list of 1 register." + f"Invalid {CONF_ADDRESS} register for fan/swing mode. Required type: positive integer, allowed 1 or list of 1 register." ) @@ -421,6 +451,12 @@ def validate_entity( loc_addr.add(f"{hub_name}{entity[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS]}_{inx}") if CONF_FAN_MODE_REGISTER in entity: loc_addr.add(f"{hub_name}{entity[CONF_FAN_MODE_REGISTER][CONF_ADDRESS]}_{inx}") + if CONF_SWING_MODE_REGISTER in entity: + loc_addr.add( + f"{hub_name}{entity[CONF_SWING_MODE_REGISTER][CONF_ADDRESS] + if isinstance(entity[CONF_SWING_MODE_REGISTER][CONF_ADDRESS],int) + else entity[CONF_SWING_MODE_REGISTER][CONF_ADDRESS][0]}_{inx}" + ) dup_addrs = ent_addr.intersection(loc_addr) if len(dup_addrs) > 0: diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 3752358c071..093dee67895 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -8,6 +8,8 @@ from homeassistant.components.climate.const import ( ATTR_FAN_MODES, ATTR_HVAC_MODE, ATTR_HVAC_MODES, + ATTR_SWING_MODE, + ATTR_SWING_MODES, FAN_AUTO, FAN_DIFFUSE, FAN_FOCUS, @@ -18,6 +20,11 @@ from homeassistant.components.climate.const import ( FAN_OFF, FAN_ON, FAN_TOP, + SWING_BOTH, + SWING_HORIZONTAL, + SWING_OFF, + SWING_ON, + SWING_VERTICAL, HVACMode, ) from homeassistant.components.modbus.const import ( @@ -45,6 +52,13 @@ from homeassistant.components.modbus.const import ( CONF_HVAC_ONOFF_REGISTER, CONF_MAX_TEMP, CONF_MIN_TEMP, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_HORIZ, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_SWING_VERT, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, @@ -58,6 +72,7 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, CONF_SLAVE, STATE_UNAVAILABLE, + STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component @@ -282,6 +297,41 @@ async def test_config_fan_mode_register(hass: HomeAssistant, mock_modbus) -> Non assert FAN_FOCUS not in state.attributes[ATTR_FAN_MODES] +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 11, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 0, + CONF_SWING_MODE_SWING_OFF: 1, + CONF_SWING_MODE_SWING_BOTH: 2, + CONF_SWING_MODE_SWING_HORIZ: 3, + CONF_SWING_MODE_SWING_VERT: 4, + }, + }, + } + ], + }, + ], +) +async def test_config_swing_mode_register(hass: HomeAssistant, mock_modbus) -> None: + """Run configuration test for Fan mode register.""" + state = hass.states.get(ENTITY_ID) + assert SWING_ON in state.attributes[ATTR_SWING_MODES] + assert SWING_OFF in state.attributes[ATTR_SWING_MODES] + assert SWING_BOTH in state.attributes[ATTR_SWING_MODES] + assert SWING_HORIZONTAL in state.attributes[ATTR_SWING_MODES] + assert SWING_VERTICAL in state.attributes[ATTR_SWING_MODES] + + @pytest.mark.parametrize( "do_config", [ @@ -572,6 +622,146 @@ async def test_service_climate_fan_update( assert hass.states.get(ENTITY_ID).attributes[ATTR_FAN_MODE] == result +@pytest.mark.parametrize( + ("do_config", "result", "register_words"), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_BOTH: 2, + }, + }, + }, + ] + }, + SWING_BOTH, + [0x02], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_VERT: 2, + }, + }, + }, + ] + }, + SWING_ON, + [0x01], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_HORIZ: 3, + }, + }, + CONF_HVAC_ONOFF_REGISTER: 119, + }, + ] + }, + SWING_HORIZONTAL, + [0x03], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_VERT: 2, + CONF_SWING_MODE_SWING_BOTH: 3, + }, + }, + }, + ] + }, + SWING_OFF, + [0x00], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 116, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_OFF: 0, + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_VERT: 2, + CONF_SWING_MODE_SWING_BOTH: 3, + }, + }, + }, + ] + }, + STATE_UNKNOWN, + [0x05], + ), + ], +) +async def test_service_climate_swing_update( + hass: HomeAssistant, mock_modbus, mock_ha, result, register_words +) -> None: + """Run test for service homeassistant.update_entity.""" + mock_modbus.read_holding_registers.return_value = ReadResult(register_words) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_ID).attributes[ATTR_SWING_MODE] == result + + @pytest.mark.parametrize( ("temperature", "result", "do_config"), [ @@ -843,6 +1033,69 @@ async def test_service_set_fan_mode( ) +@pytest.mark.parametrize( + ("swing_mode", "result", "do_config"), + [ + ( + SWING_OFF, + [0x00], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [118], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_OFF: 0, + }, + }, + } + ] + }, + ), + ( + SWING_ON, + [0x01], + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 117, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 118, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 1, + CONF_SWING_MODE_SWING_OFF: 0, + }, + }, + } + ] + }, + ), + ], +) +async def test_service_set_swing_mode( + hass: HomeAssistant, swing_mode, result, mock_modbus, mock_ha +) -> None: + """Test set Swing mode.""" + mock_modbus.read_holding_registers.return_value = ReadResult(result) + await hass.services.async_call( + CLIMATE_DOMAIN, + "set_swing_mode", + { + "entity_id": ENTITY_ID, + ATTR_SWING_MODE: swing_mode, + }, + blocking=True, + ) + + test_value = State(ENTITY_ID, 35) test_value.attributes = {ATTR_TEMPERATURE: 37} diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 0ca4703aa5f..922022741b0 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -66,6 +66,11 @@ from homeassistant.components.modbus.const import ( CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, + CONF_SWING_MODE_REGISTER, + CONF_SWING_MODE_SWING_BOTH, + CONF_SWING_MODE_SWING_OFF, + CONF_SWING_MODE_SWING_ON, + CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, CONF_VIRTUAL_COUNT, DEFAULT_SCAN_INTERVAL, @@ -84,6 +89,7 @@ from homeassistant.components.modbus.validators import ( check_config, check_hvac_target_temp_registers, duplicate_fan_mode_validator, + duplicate_swing_mode_validator, hvac_fixedsize_reglist_validator, nan_validator, register_int_list_validator, @@ -629,6 +635,42 @@ async def test_check_config_sensor(hass: HomeAssistant, do_config) -> None: ], } ], + [ # Testing Swing modes + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 117, + CONF_SLAVE: 0, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 120, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 0, + CONF_SWING_MODE_SWING_BOTH: 1, + }, + }, + }, + { + CONF_NAME: TEST_ENTITY_NAME + " 2", + CONF_ADDRESS: 119, + CONF_SLAVE: 0, + CONF_TARGET_TEMP: 118, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [120], + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 0, + CONF_SWING_MODE_SWING_BOTH: 1, + }, + }, + }, + ], + } + ], [ { CONF_NAME: TEST_MODBUS_NAME, @@ -733,6 +775,29 @@ async def test_check_config_climate(hass: HomeAssistant, do_config) -> None: CONF_FAN_MODE_REGISTER: { CONF_ADDRESS: 117, }, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: 117, + }, + }, + ], + [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1, + CONF_TARGET_TEMP: [117], + CONF_HVAC_MODE_REGISTER: { + CONF_ADDRESS: 117, + CONF_HVAC_MODE_VALUES: { + CONF_HVAC_MODE_COOL: 0, + CONF_HVAC_MODE_HEAT: 1, + CONF_HVAC_MODE_HEAT_COOL: 2, + CONF_HVAC_MODE_DRY: 3, + }, + }, + CONF_HVAC_ONOFF_REGISTER: 117, + CONF_SWING_MODE_REGISTER: { + CONF_ADDRESS: [117], + }, }, ], ], @@ -743,6 +808,7 @@ async def test_climate_conflict_addresses(do_config) -> None: assert CONF_HVAC_MODE_REGISTER not in do_config[0] assert CONF_HVAC_ONOFF_REGISTER not in do_config[0] assert CONF_FAN_MODE_REGISTER not in do_config[0] + assert CONF_SWING_MODE_REGISTER not in do_config[0] @pytest.mark.parametrize( @@ -764,6 +830,25 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: assert len(do_config[CONF_FAN_MODE_VALUES]) == 2 +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 11, + CONF_SWING_MODE_VALUES: { + CONF_SWING_MODE_SWING_ON: 7, + CONF_SWING_MODE_SWING_OFF: 9, + CONF_SWING_MODE_SWING_BOTH: 9, + }, + } + ], +) +async def test_duplicate_swing_mode_validator(do_config) -> None: + """Test duplicate modbus validator.""" + duplicate_swing_mode_validator(do_config) + assert len(do_config[CONF_SWING_MODE_VALUES]) == 2 + + @pytest.mark.parametrize( ("do_config", "sensor_cnt"), [