From 288bb62b56b791e2d0f6504ce0e15ec0e010fd61 Mon Sep 17 00:00:00 2001 From: Illia Piskurov Date: Thu, 21 Aug 2025 09:07:44 +0000 Subject: [PATCH 01/13] Support of current temp scale and offset --- homeassistant/components/modbus/__init__.py | 4 + homeassistant/components/modbus/climate.py | 19 ++++- homeassistant/components/modbus/const.py | 2 + tests/components/modbus/test_climate.py | 81 +++++++++++++++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index ab387030af8ec..31fc0f3790643 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -66,6 +66,8 @@ CONF_BYTESIZE, CONF_CLIMATES, CONF_COLOR_TEMP_REGISTER, + CONF_CURRENT_TEMP_OFFSET, + CONF_CURRENT_TEMP_SCALE, CONF_DATA_TYPE, CONF_DEVICE_ADDRESS, CONF_FAN_MODE_AUTO, @@ -212,6 +214,8 @@ vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Inclusive(CONF_CURRENT_TEMP_SCALE, "current_temp"): vol.Coerce(float), + vol.Inclusive(CONF_CURRENT_TEMP_OFFSET, "current_temp"): vol.Coerce(float), vol.Optional(CONF_PRECISION): cv.positive_int, vol.Optional( CONF_SWAP, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index f8e7dca245a61..e759c60d7b3a5 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -50,6 +50,8 @@ CALL_TYPE_WRITE_REGISTER, CALL_TYPE_WRITE_REGISTERS, CONF_CLIMATES, + CONF_CURRENT_TEMP_OFFSET, + CONF_CURRENT_TEMP_SCALE, CONF_FAN_MODE_AUTO, CONF_FAN_MODE_DIFFUSE, CONF_FAN_MODE_FOCUS, @@ -301,6 +303,13 @@ def __init__( else: self._hvac_onoff_coil = None + if CONF_CURRENT_TEMP_SCALE in config: + self._current_temp_scale = config[CONF_CURRENT_TEMP_SCALE] + self._current_temp_offset = config[CONF_CURRENT_TEMP_OFFSET] + else: + self._current_temp_scale = None + self._current_temp_offset = None + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() @@ -474,9 +483,17 @@ async def _async_update(self) -> None: ], ) - self._attr_current_temperature = await self._async_read_register( + current_temp_register_value = await self._async_read_register( self._input_type, self._address ) + if self._current_temp_scale is None: + self._attr_current_temperature = current_temp_register_value + else: + # Undo global scale/offset + raw_value = (current_temp_register_value - self._offset) / self._scale + new_value = raw_value * self._current_temp_scale + self._current_temp_offset + self._attr_current_temperature = new_value + # Read the HVAC mode register if defined if self._hvac_mode_register is not None: hvac_mode = await self._async_read_register( diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index dafc604e7812a..addc7c08c0c01 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -19,6 +19,8 @@ CONF_CLIMATES = "climates" CONF_BRIGHTNESS_REGISTER = "brightness_address" CONF_COLOR_TEMP_REGISTER = "color_temp_address" +CONF_CURRENT_TEMP_SCALE = "current_temp_scale" +CONF_CURRENT_TEMP_OFFSET = "current_temp_offset" CONF_DATA_TYPE = "data_type" CONF_DEVICE_ADDRESS = "device_address" CONF_FANS = "fans" diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index f661dd2083cc0..b76cf754a6ab7 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -38,6 +38,8 @@ from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY from homeassistant.components.modbus.const import ( CONF_CLIMATES, + CONF_CURRENT_TEMP_OFFSET, + CONF_CURRENT_TEMP_SCALE, CONF_DATA_TYPE, CONF_DEVICE_ADDRESS, CONF_FAN_MODE_AUTO, @@ -74,6 +76,7 @@ CONF_HVAC_ONOFF_REGISTER, CONF_MAX_TEMP, CONF_MIN_TEMP, + CONF_SCALE, CONF_SWING_MODE_REGISTER, CONF_SWING_MODE_SWING_BOTH, CONF_SWING_MODE_SWING_HORIZ, @@ -92,6 +95,7 @@ ATTR_TEMPERATURE, CONF_ADDRESS, CONF_NAME, + CONF_OFFSET, CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_SLAVE, @@ -928,6 +932,83 @@ async def test_hvac_onoff_register_transition_update( assert state.state == result_after +@pytest.mark.parametrize( + ("do_config", "result", "register_words"), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + }, + ] + }, + 17, + [0, 17], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_CURRENT_TEMP_SCALE: 0.01, + CONF_CURRENT_TEMP_OFFSET: 10, + CONF_SCALE: 10, + CONF_OFFSET: 20, + }, + ] + }, + 35, + [2500], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_CURRENT_TEMP_SCALE: 0.01, + CONF_CURRENT_TEMP_OFFSET: 10, + CONF_SCALE: 10, + CONF_OFFSET: -20, + }, + ] + }, + 29, + [1900], + ), + ], +) +async def test_config_current_temp_scale_and_offset( + hass: HomeAssistant, mock_modbus_ha, result, register_words +) -> None: + """Test behavior with different configurations for temperature scaling.""" + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes.get("current_temperature") == result + + @pytest.mark.parametrize( ("do_config", "result", "register_words"), [ From 6feb103ae4fb55addf9bb49c429047157917bf3b Mon Sep 17 00:00:00 2001 From: Illia Piskurov Date: Fri, 22 Aug 2025 07:40:05 +0000 Subject: [PATCH 02/13] Try another way --- homeassistant/components/modbus/__init__.py | 8 ++- homeassistant/components/modbus/climate.py | 78 ++++++++++++++------ homeassistant/components/modbus/const.py | 2 + homeassistant/components/modbus/entity.py | 16 +++-- tests/components/modbus/test_climate.py | 79 +++++++++++++++++++++ 5 files changed, 155 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 31fc0f3790643..94d135b5320da 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -139,6 +139,8 @@ CONF_SWING_MODE_SWING_VERT, CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, + CONF_TARGET_TEMP_OFFSET, + CONF_TARGET_TEMP_SCALE, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_VERIFY, CONF_VIRTUAL_COUNT, @@ -214,8 +216,6 @@ vol.Optional(CONF_STRUCTURE): cv.string, vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), - vol.Inclusive(CONF_CURRENT_TEMP_SCALE, "current_temp"): vol.Coerce(float), - vol.Inclusive(CONF_CURRENT_TEMP_OFFSET, "current_temp"): vol.Coerce(float), vol.Optional(CONF_PRECISION): cv.positive_int, vol.Optional( CONF_SWAP, @@ -277,6 +277,10 @@ vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int, vol.Exclusive(CONF_HVAC_ONOFF_REGISTER, "hvac_onoff_type"): cv.positive_int, + vol.Inclusive(CONF_CURRENT_TEMP_SCALE, "current_temp"): vol.Coerce(float), + vol.Inclusive(CONF_CURRENT_TEMP_OFFSET, "current_temp"): vol.Coerce(float), + vol.Inclusive(CONF_TARGET_TEMP_SCALE, "target_temp"): vol.Coerce(float), + vol.Inclusive(CONF_TARGET_TEMP_OFFSET, "target_temp"): vol.Coerce(float), vol.Optional( CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE ): cv.positive_int, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index e759c60d7b3a5..d80d8cd8ac780 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -99,6 +99,8 @@ CONF_SWING_MODE_SWING_VERT, CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, + CONF_TARGET_TEMP_OFFSET, + CONF_TARGET_TEMP_SCALE, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, DataType, @@ -303,13 +305,20 @@ def __init__( else: self._hvac_onoff_coil = None - if CONF_CURRENT_TEMP_SCALE in config: + if CONF_CURRENT_TEMP_SCALE in config and CONF_CURRENT_TEMP_OFFSET in config: self._current_temp_scale = config[CONF_CURRENT_TEMP_SCALE] self._current_temp_offset = config[CONF_CURRENT_TEMP_OFFSET] else: self._current_temp_scale = None self._current_temp_offset = None + if CONF_TARGET_TEMP_SCALE in config and CONF_TARGET_TEMP_OFFSET in config: + self._target_temp_scale = config[CONF_TARGET_TEMP_SCALE] + self._target_temp_offset = config[CONF_TARGET_TEMP_OFFSET] + else: + self._target_temp_scale = None + self._target_temp_offset = None + async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() @@ -421,9 +430,14 @@ async def async_set_swing_mode(self, swing_mode: str) -> None: async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - target_temperature = ( - float(kwargs[ATTR_TEMPERATURE]) - self._offset - ) / self._scale + if self._target_temp_scale is not None and self._target_temp_offset is not None: + target_temperature = ( + float(kwargs[ATTR_TEMPERATURE]) - self._target_temp_offset + ) / self._target_temp_scale + else: + target_temperature = ( + float(kwargs[ATTR_TEMPERATURE]) - self._offset + ) / self._scale if self._data_type in ( DataType.INT16, DataType.INT32, @@ -476,23 +490,41 @@ async def async_set_temperature(self, **kwargs: Any) -> None: async def _async_update(self) -> None: """Update Target & Current Temperature.""" - self._attr_target_temperature = await self._async_read_register( - CALL_TYPE_REGISTER_HOLDING, - self._target_temperature_register[ - HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode] - ], - ) + if self._target_temp_scale is not None and self._target_temp_offset is not None: + target_temp_register_value = await self._async_read_register( + CALL_TYPE_REGISTER_HOLDING, + self._target_temperature_register[ + HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode] + ], + skip_transform=True, + ) + self._attr_target_temperature = ( + self._target_temp_scale * target_temp_register_value + + self._target_temp_offset + ) + else: + self._attr_target_temperature = await self._async_read_register( + CALL_TYPE_REGISTER_HOLDING, + self._target_temperature_register[ + HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode] + ], + ) - current_temp_register_value = await self._async_read_register( - self._input_type, self._address - ) - if self._current_temp_scale is None: - self._attr_current_temperature = current_temp_register_value + if ( + self._current_temp_scale is not None + and self._current_temp_offset is not None + ): + current_temp_register_value = await self._async_read_register( + self._input_type, self._address, skip_transform=True + ) + self._attr_current_temperature = ( + self._current_temp_scale * current_temp_register_value + + self._current_temp_offset + ) else: - # Undo global scale/offset - raw_value = (current_temp_register_value - self._offset) / self._scale - new_value = raw_value * self._current_temp_scale + self._current_temp_offset - self._attr_current_temperature = new_value + self._attr_current_temperature = await self._async_read_register( + self._input_type, self._address + ) # Read the HVAC mode register if defined if self._hvac_mode_register is not None: @@ -579,7 +611,11 @@ async def _async_update(self) -> None: self._attr_hvac_mode = HVACMode.OFF async def _async_read_register( - self, register_type: str, register: int, raw: bool | None = False + self, + register_type: str, + register: int, + raw: bool | None = False, + skip_transform: bool | None = False, ) -> float | None: """Read register using the Modbus hub slave.""" result = await self._hub.async_pb_call( @@ -596,7 +632,7 @@ async def _async_read_register( return int(result.registers[0]) # The regular handling of the value - self._value = self.unpack_structure_result(result.registers) + self._value = self.unpack_structure_result(result.registers, skip_transform) if not self._value: self._attr_available = False return None diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index addc7c08c0c01..ea5a8fcfd4087 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -21,6 +21,8 @@ CONF_COLOR_TEMP_REGISTER = "color_temp_address" CONF_CURRENT_TEMP_SCALE = "current_temp_scale" CONF_CURRENT_TEMP_OFFSET = "current_temp_offset" +CONF_TARGET_TEMP_SCALE = "target_temp_scale" +CONF_TARGET_TEMP_OFFSET = "target_temp_offset" CONF_DATA_TYPE = "data_type" CONF_DEVICE_ADDRESS = "device_address" CONF_FANS = "fans" diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 689d882a2f37b..b8dc04052294d 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -256,7 +256,9 @@ def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: registers.reverse() return registers - def __process_raw_value(self, entry: float | str | bytes) -> str | None: + def __process_raw_value( + self, entry: float | str | bytes, skip_transform: bool | None = False + ) -> str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" if self._nan_value and entry in (self._nan_value, -self._nan_value): return None @@ -265,7 +267,9 @@ def __process_raw_value(self, entry: float | str | bytes) -> str | None: if entry != entry: # noqa: PLR0124 # NaN float detection replace with None return None - val: float | int = self._scale * entry + self._offset + val: float | int = cast(float | int, entry) + if not skip_transform: + val = self._scale * entry + self._offset if self._min_value is not None and val < self._min_value: val = self._min_value if self._max_value is not None and val > self._max_value: @@ -276,7 +280,9 @@ def __process_raw_value(self, entry: float | str | bytes) -> str | None: return str(round(val)) return f"{float(val):.{self._precision}f}" - def unpack_structure_result(self, registers: list[int]) -> str | None: + def unpack_structure_result( + self, registers: list[int], skip_transform: bool | None = False + ) -> str | None: """Convert registers to proper result.""" if self._swap: @@ -298,7 +304,7 @@ def unpack_structure_result(self, registers: list[int]) -> str | None: # Apply scale, precision, limits to floats and ints v_result = [] for entry in val: - v_temp = self.__process_raw_value(entry) + v_temp = self.__process_raw_value(entry, skip_transform) if self._data_type != DataType.CUSTOM: v_result.append(str(v_temp)) else: @@ -306,7 +312,7 @@ def unpack_structure_result(self, registers: list[int]) -> str | None: return ",".join(map(str, v_result)) # Apply scale, precision, limits to floats and ints - return self.__process_raw_value(val[0]) + return self.__process_raw_value(val[0], skip_transform) class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index b76cf754a6ab7..8ec9ce1d1ff71 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -85,6 +85,8 @@ CONF_SWING_MODE_SWING_VERT, CONF_SWING_MODE_VALUES, CONF_TARGET_TEMP, + CONF_TARGET_TEMP_OFFSET, + CONF_TARGET_TEMP_SCALE, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, MODBUS_DOMAIN, @@ -1009,6 +1011,83 @@ async def test_config_current_temp_scale_and_offset( assert state.attributes.get("current_temperature") == result +@pytest.mark.parametrize( + ("do_config", "result", "register_words"), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_DATA_TYPE: DataType.INT32, + }, + ] + }, + 17, + [0, 17], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_TARGET_TEMP_SCALE: 0.01, + CONF_TARGET_TEMP_OFFSET: -5, + CONF_SCALE: 10, + CONF_OFFSET: 20, + }, + ] + }, + 20, + [2500], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_TARGET_TEMP_SCALE: 0.1, + CONF_TARGET_TEMP_OFFSET: 5, + CONF_SCALE: 10, + CONF_OFFSET: -20, + }, + ] + }, + 26, + [210], + ), + ], +) +async def test_config_target_temp_scale_and_offset( + hass: HomeAssistant, mock_modbus_ha, result, register_words +) -> None: + """Test behavior with different configurations for temperature scaling.""" + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes.get("temperature") == result + + @pytest.mark.parametrize( ("do_config", "result", "register_words"), [ From 5061afb4a79d4543d8b72f7bc48124892843b65c Mon Sep 17 00:00:00 2001 From: Illia Piskurov Date: Mon, 1 Sep 2025 16:14:53 +0300 Subject: [PATCH 03/13] Separated offset and scale --- homeassistant/components/modbus/__init__.py | 8 +- homeassistant/components/modbus/climate.py | 83 +++++++-------------- homeassistant/components/modbus/entity.py | 23 ++++-- 3 files changed, 45 insertions(+), 69 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 94d135b5320da..aa877bb7d91a4 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -277,10 +277,10 @@ vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int, vol.Exclusive(CONF_HVAC_ONOFF_REGISTER, "hvac_onoff_type"): cv.positive_int, - vol.Inclusive(CONF_CURRENT_TEMP_SCALE, "current_temp"): vol.Coerce(float), - vol.Inclusive(CONF_CURRENT_TEMP_OFFSET, "current_temp"): vol.Coerce(float), - vol.Inclusive(CONF_TARGET_TEMP_SCALE, "target_temp"): vol.Coerce(float), - vol.Inclusive(CONF_TARGET_TEMP_OFFSET, "target_temp"): vol.Coerce(float), + vol.Optional(CONF_CURRENT_TEMP_SCALE): vol.Coerce(float), + vol.Optional(CONF_CURRENT_TEMP_OFFSET): vol.Coerce(float), + vol.Optional(CONF_TARGET_TEMP_SCALE): vol.Coerce(float), + vol.Optional(CONF_TARGET_TEMP_OFFSET): vol.Coerce(float), vol.Optional( CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE ): cv.positive_int, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index d80d8cd8ac780..89111bd41b005 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -170,6 +170,10 @@ def __init__( self._attr_min_temp = config[CONF_MIN_TEMP] self._attr_max_temp = config[CONF_MAX_TEMP] self._attr_target_temperature_step = config[CONF_STEP] + self._current_temp_scale = config.get(CONF_CURRENT_TEMP_SCALE) + self._current_temp_offset = config.get(CONF_CURRENT_TEMP_OFFSET) + self._target_temp_scale = config.get(CONF_TARGET_TEMP_SCALE) + self._target_temp_offset = config.get(CONF_TARGET_TEMP_OFFSET) if CONF_HVAC_MODE_REGISTER in config: mode_config = config[CONF_HVAC_MODE_REGISTER] @@ -305,20 +309,6 @@ def __init__( else: self._hvac_onoff_coil = None - if CONF_CURRENT_TEMP_SCALE in config and CONF_CURRENT_TEMP_OFFSET in config: - self._current_temp_scale = config[CONF_CURRENT_TEMP_SCALE] - self._current_temp_offset = config[CONF_CURRENT_TEMP_OFFSET] - else: - self._current_temp_scale = None - self._current_temp_offset = None - - if CONF_TARGET_TEMP_SCALE in config and CONF_TARGET_TEMP_OFFSET in config: - self._target_temp_scale = config[CONF_TARGET_TEMP_SCALE] - self._target_temp_offset = config[CONF_TARGET_TEMP_OFFSET] - else: - self._target_temp_scale = None - self._target_temp_offset = None - async def async_added_to_hass(self) -> None: """Handle entity which will be added.""" await self.async_base_added_to_hass() @@ -430,14 +420,10 @@ async def async_set_swing_mode(self, swing_mode: str) -> None: async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - if self._target_temp_scale is not None and self._target_temp_offset is not None: - target_temperature = ( - float(kwargs[ATTR_TEMPERATURE]) - self._target_temp_offset - ) / self._target_temp_scale - else: - target_temperature = ( - float(kwargs[ATTR_TEMPERATURE]) - self._offset - ) / self._scale + scale = self._target_temp_scale or self._scale + offset = self._target_temp_offset or self._offset + + target_temperature = (float(kwargs[ATTR_TEMPERATURE]) - offset) / scale if self._data_type in ( DataType.INT16, DataType.INT32, @@ -490,41 +476,21 @@ async def async_set_temperature(self, **kwargs: Any) -> None: async def _async_update(self) -> None: """Update Target & Current Temperature.""" - if self._target_temp_scale is not None and self._target_temp_offset is not None: - target_temp_register_value = await self._async_read_register( - CALL_TYPE_REGISTER_HOLDING, - self._target_temperature_register[ - HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode] - ], - skip_transform=True, - ) - self._attr_target_temperature = ( - self._target_temp_scale * target_temp_register_value - + self._target_temp_offset - ) - else: - self._attr_target_temperature = await self._async_read_register( - CALL_TYPE_REGISTER_HOLDING, - self._target_temperature_register[ - HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode] - ], - ) + self._attr_target_temperature = await self._async_read_register( + CALL_TYPE_REGISTER_HOLDING, + self._target_temperature_register[ + HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode] + ], + scale=self._target_temp_scale or self._scale, + offset=self._target_temp_offset or self._offset, + ) - if ( - self._current_temp_scale is not None - and self._current_temp_offset is not None - ): - current_temp_register_value = await self._async_read_register( - self._input_type, self._address, skip_transform=True - ) - self._attr_current_temperature = ( - self._current_temp_scale * current_temp_register_value - + self._current_temp_offset - ) - else: - self._attr_current_temperature = await self._async_read_register( - self._input_type, self._address - ) + self._attr_current_temperature = await self._async_read_register( + self._input_type, + self._address, + scale=self._current_temp_scale or self._scale, + offset=self._current_temp_offset or self._offset, + ) # Read the HVAC mode register if defined if self._hvac_mode_register is not None: @@ -614,8 +580,9 @@ async def _async_read_register( self, register_type: str, register: int, + scale: float | None = None, + offset: float | None = None, raw: bool | None = False, - skip_transform: bool | None = False, ) -> float | None: """Read register using the Modbus hub slave.""" result = await self._hub.async_pb_call( @@ -632,7 +599,7 @@ async def _async_read_register( return int(result.registers[0]) # The regular handling of the value - self._value = self.unpack_structure_result(result.registers, skip_transform) + self._value = self.unpack_structure_result(result.registers, scale, offset) if not self._value: self._attr_available = False return None diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index b8dc04052294d..785ba8d4cfc34 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -257,9 +257,20 @@ def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: return registers def __process_raw_value( - self, entry: float | str | bytes, skip_transform: bool | None = False + self, + entry: float | str | bytes, + scale: float | None = None, + offset: float | None = None, ) -> str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" + if scale is None: + scale = self._scale + if offset is None: + offset = self._offset + + assert scale is not None + assert offset is not None + if self._nan_value and entry in (self._nan_value, -self._nan_value): return None if isinstance(entry, bytes): @@ -267,9 +278,7 @@ def __process_raw_value( if entry != entry: # noqa: PLR0124 # NaN float detection replace with None return None - val: float | int = cast(float | int, entry) - if not skip_transform: - val = self._scale * entry + self._offset + val: float | int = scale * float(entry) + offset if self._min_value is not None and val < self._min_value: val = self._min_value if self._max_value is not None and val > self._max_value: @@ -281,7 +290,7 @@ def __process_raw_value( return f"{float(val):.{self._precision}f}" def unpack_structure_result( - self, registers: list[int], skip_transform: bool | None = False + self, registers: list[int], scale: float | None, offset: float | None ) -> str | None: """Convert registers to proper result.""" @@ -304,7 +313,7 @@ def unpack_structure_result( # Apply scale, precision, limits to floats and ints v_result = [] for entry in val: - v_temp = self.__process_raw_value(entry, skip_transform) + v_temp = self.__process_raw_value(entry, scale, offset) if self._data_type != DataType.CUSTOM: v_result.append(str(v_temp)) else: @@ -312,7 +321,7 @@ def unpack_structure_result( return ",".join(map(str, v_result)) # Apply scale, precision, limits to floats and ints - return self.__process_raw_value(val[0], skip_transform) + return self.__process_raw_value(val[0], scale, offset) class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity): From ab50e43d75e563e5e9b20434f5e386cb10dbf478 Mon Sep 17 00:00:00 2001 From: Illia Piskurov Date: Tue, 2 Sep 2025 10:48:33 +0300 Subject: [PATCH 04/13] Removed mistakes --- homeassistant/components/modbus/climate.py | 27 ++++++++++++---------- homeassistant/components/modbus/entity.py | 18 ++++----------- 2 files changed, 20 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 89111bd41b005..00ffabe6e558b 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -170,10 +170,10 @@ def __init__( self._attr_min_temp = config[CONF_MIN_TEMP] self._attr_max_temp = config[CONF_MAX_TEMP] self._attr_target_temperature_step = config[CONF_STEP] - self._current_temp_scale = config.get(CONF_CURRENT_TEMP_SCALE) - self._current_temp_offset = config.get(CONF_CURRENT_TEMP_OFFSET) - self._target_temp_scale = config.get(CONF_TARGET_TEMP_SCALE) - self._target_temp_offset = config.get(CONF_TARGET_TEMP_OFFSET) + self._current_temp_scale = config.get(CONF_CURRENT_TEMP_SCALE) or self._scale + self._current_temp_offset = config.get(CONF_CURRENT_TEMP_OFFSET) or self._offset + self._target_temp_scale = config.get(CONF_TARGET_TEMP_SCALE) or self._scale + self._target_temp_offset = config.get(CONF_TARGET_TEMP_OFFSET) or self._offset if CONF_HVAC_MODE_REGISTER in config: mode_config = config[CONF_HVAC_MODE_REGISTER] @@ -420,10 +420,9 @@ async def async_set_swing_mode(self, swing_mode: str) -> None: async def async_set_temperature(self, **kwargs: Any) -> None: """Set new target temperature.""" - scale = self._target_temp_scale or self._scale - offset = self._target_temp_offset or self._offset - - target_temperature = (float(kwargs[ATTR_TEMPERATURE]) - offset) / scale + target_temperature = ( + float(kwargs[ATTR_TEMPERATURE]) - self._target_temp_offset + ) / self._target_temp_scale if self._data_type in ( DataType.INT16, DataType.INT32, @@ -481,15 +480,15 @@ async def _async_update(self) -> None: self._target_temperature_register[ HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode] ], - scale=self._target_temp_scale or self._scale, - offset=self._target_temp_offset or self._offset, + scale=self._target_temp_scale, + offset=self._target_temp_offset, ) self._attr_current_temperature = await self._async_read_register( self._input_type, self._address, - scale=self._current_temp_scale or self._scale, - offset=self._current_temp_offset or self._offset, + scale=self._current_temp_scale, + offset=self._current_temp_offset, ) # Read the HVAC mode register if defined @@ -599,6 +598,10 @@ async def _async_read_register( return int(result.registers[0]) # The regular handling of the value + if scale is None: + scale = self._scale + if offset is None: + offset = self._offset self._value = self.unpack_structure_result(result.registers, scale, offset) if not self._value: self._attr_available = False diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 785ba8d4cfc34..c32ff5530ea55 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -213,8 +213,8 @@ def __init__(self, hass: HomeAssistant, hub: ModbusHub, config: dict) -> None: self._swap = config[CONF_SWAP] self._data_type = config[CONF_DATA_TYPE] self._structure: str = config[CONF_STRUCTURE] - self._scale = config[CONF_SCALE] - self._offset = config[CONF_OFFSET] + self._scale: float | int = config[CONF_SCALE] + self._offset: float | int = config[CONF_OFFSET] self._slave_count = config.get(CONF_SLAVE_COUNT) or config.get( CONF_VIRTUAL_COUNT, 0 ) @@ -259,18 +259,10 @@ def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: def __process_raw_value( self, entry: float | str | bytes, - scale: float | None = None, - offset: float | None = None, + scale: float, + offset: float, ) -> str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" - if scale is None: - scale = self._scale - if offset is None: - offset = self._offset - - assert scale is not None - assert offset is not None - if self._nan_value and entry in (self._nan_value, -self._nan_value): return None if isinstance(entry, bytes): @@ -290,7 +282,7 @@ def __process_raw_value( return f"{float(val):.{self._precision}f}" def unpack_structure_result( - self, registers: list[int], scale: float | None, offset: float | None + self, registers: list[int], scale: float, offset: float ) -> str | None: """Convert registers to proper result.""" From 29b3202e199de7d305c518c5cea2de8115a8a17d Mon Sep 17 00:00:00 2001 From: Illia Piskurov Date: Tue, 2 Sep 2025 11:06:54 +0300 Subject: [PATCH 05/13] Change in sensor.py --- homeassistant/components/modbus/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index a11e25b4dd47b..2e71f84c9a154 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -118,7 +118,9 @@ async def _async_update(self) -> None: self.async_write_ha_state() return self._attr_available = True - result = self.unpack_structure_result(raw_result.registers) + result = self.unpack_structure_result( + raw_result.registers, self._scale, self._offset + ) if self._coordinator: result_array: list[float | None] = [] if result: From 818221df9923ac4d2a08bfa117055c721616c947 Mon Sep 17 00:00:00 2001 From: crug80 Date: Fri, 5 Sep 2025 14:54:18 +0000 Subject: [PATCH 06/13] rev2-crug --- homeassistant/components/modbus/__init__.py | 24 +- homeassistant/components/modbus/climate.py | 16 +- homeassistant/components/modbus/const.py | 9 +- homeassistant/components/modbus/entity.py | 14 +- homeassistant/components/modbus/validators.py | 62 ++++ tests/components/modbus/test_climate.py | 300 +++++++++--------- tests/components/modbus/test_init.py | 80 +++++ 7 files changed, 325 insertions(+), 180 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index c25b3293ece25..d75a3b28dea2b 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -150,6 +150,8 @@ DEFAULT_HUB, DEFAULT_HVAC_OFF_VALUE, DEFAULT_HVAC_ON_VALUE, + DEFAULT_OFFSET, + DEFAULT_SCALE, DEFAULT_SCAN_INTERVAL, DEFAULT_TEMP_UNIT, MODBUS_DOMAIN as DOMAIN, @@ -163,6 +165,7 @@ from .validators import ( duplicate_fan_mode_validator, duplicate_swing_mode_validator, + ensure_and_check_conflicting_scales_and_offsets, hvac_fixedsize_reglist_validator, nan_validator, register_int_list_validator, @@ -214,8 +217,8 @@ ] ), vol.Optional(CONF_STRUCTURE): cv.string, - vol.Optional(CONF_SCALE, default=1): vol.Coerce(float), - vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float), + vol.Optional(CONF_SCALE, default=DEFAULT_SCALE): vol.Coerce(float), + vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(float), vol.Optional(CONF_PRECISION): cv.positive_int, vol.Optional( CONF_SWAP, @@ -277,10 +280,18 @@ vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int, vol.Exclusive(CONF_HVAC_ONOFF_REGISTER, "hvac_onoff_type"): cv.positive_int, - vol.Optional(CONF_CURRENT_TEMP_SCALE): vol.Coerce(float), - vol.Optional(CONF_CURRENT_TEMP_OFFSET): vol.Coerce(float), - vol.Optional(CONF_TARGET_TEMP_SCALE): vol.Coerce(float), - vol.Optional(CONF_TARGET_TEMP_OFFSET): vol.Coerce(float), + vol.Optional(CONF_CURRENT_TEMP_SCALE, default=DEFAULT_SCALE): vol.Coerce( + float + ), + vol.Optional(CONF_TARGET_TEMP_SCALE, default=DEFAULT_SCALE): vol.Coerce( + float + ), + vol.Optional(CONF_CURRENT_TEMP_OFFSET, default=DEFAULT_OFFSET): vol.Coerce( + float + ), + vol.Optional(CONF_TARGET_TEMP_OFFSET, default=DEFAULT_OFFSET): vol.Coerce( + float + ), vol.Optional( CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE ): cv.positive_int, @@ -393,6 +404,7 @@ ), }, ), + ensure_and_check_conflicting_scales_and_offsets, ) COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend( diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index bd1ffc8a3f818..7e98cd4b675b5 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -170,10 +170,10 @@ def __init__( self._attr_min_temp = config[CONF_MIN_TEMP] self._attr_max_temp = config[CONF_MAX_TEMP] self._attr_target_temperature_step = config[CONF_STEP] - self._current_temp_scale = config.get(CONF_CURRENT_TEMP_SCALE) or self._scale - self._current_temp_offset = config.get(CONF_CURRENT_TEMP_OFFSET) or self._offset - self._target_temp_scale = config.get(CONF_TARGET_TEMP_SCALE) or self._scale - self._target_temp_offset = config.get(CONF_TARGET_TEMP_OFFSET) or self._offset + self._current_temp_scale = config[CONF_CURRENT_TEMP_SCALE] + self._current_temp_offset = config[CONF_CURRENT_TEMP_OFFSET] + self._target_temp_scale = config[CONF_TARGET_TEMP_SCALE] + self._target_temp_offset = config[CONF_TARGET_TEMP_OFFSET] if CONF_HVAC_MODE_REGISTER in config: mode_config = config[CONF_HVAC_MODE_REGISTER] @@ -579,8 +579,8 @@ async def _async_read_register( self, register_type: str, register: int, - scale: float | None = None, - offset: float | None = None, + scale: float = 1, + offset: float = 0, raw: bool | None = False, ) -> float | None: """Read register using the Modbus hub slave.""" @@ -598,10 +598,6 @@ async def _async_read_register( return int(result.registers[0]) # The regular handling of the value - if scale is None: - scale = self._scale - if offset is None: - offset = self._offset self._value = self.unpack_structure_result(result.registers, scale, offset) if not self._value: self._attr_available = False diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py index ea5a8fcfd4087..7b85b61287c36 100644 --- a/homeassistant/components/modbus/const.py +++ b/homeassistant/components/modbus/const.py @@ -19,10 +19,8 @@ CONF_CLIMATES = "climates" CONF_BRIGHTNESS_REGISTER = "brightness_address" CONF_COLOR_TEMP_REGISTER = "color_temp_address" -CONF_CURRENT_TEMP_SCALE = "current_temp_scale" CONF_CURRENT_TEMP_OFFSET = "current_temp_offset" -CONF_TARGET_TEMP_SCALE = "target_temp_scale" -CONF_TARGET_TEMP_OFFSET = "target_temp_offset" +CONF_CURRENT_TEMP_SCALE = "current_temp_scale" CONF_DATA_TYPE = "data_type" CONF_DEVICE_ADDRESS = "device_address" CONF_FANS = "fans" @@ -52,6 +50,8 @@ CONF_SWAP_WORD = "word" CONF_SWAP_WORD_BYTE = "word_byte" CONF_TARGET_TEMP = "target_temp_register" +CONF_TARGET_TEMP_OFFSET = "target_temp_offset" +CONF_TARGET_TEMP_SCALE = "target_temp_scale" CONF_TARGET_TEMP_WRITE_REGISTERS = "target_temp_write_registers" CONF_FAN_MODE_REGISTER = "fan_mode_register" CONF_FAN_MODE_ON = "state_fan_on" @@ -184,4 +184,7 @@ class DataType(str, Enum): LIGHT_MODBUS_SCALE_MAX = 100 LIGHT_MODBUS_INVALID_VALUE = 0xFFFF +DEFAULT_SCALE = 1.0 +DEFAULT_OFFSET = 0 + _LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index d55aacbeaf3e6..9c2266d84dab8 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -178,8 +178,8 @@ def __init__(self, hass: HomeAssistant, hub: ModbusHub, config: dict) -> None: self._swap = config[CONF_SWAP] self._data_type = config[CONF_DATA_TYPE] self._structure: str = config[CONF_STRUCTURE] - self._scale: float | int = config[CONF_SCALE] - self._offset: float | int = config[CONF_OFFSET] + self._scale = config[CONF_SCALE] + self._offset = config[CONF_OFFSET] self._slave_count = config.get(CONF_SLAVE_COUNT) or config.get( CONF_VIRTUAL_COUNT, 0 ) @@ -223,9 +223,9 @@ def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: def __process_raw_value( self, - entry: float | str | bytes, - scale: float, - offset: float, + entry: float | bytes, + scale: float = 1, + offset: float = 0, ) -> str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" if self._nan_value and entry in (self._nan_value, -self._nan_value): @@ -235,7 +235,7 @@ def __process_raw_value( if entry != entry: # noqa: PLR0124 # NaN float detection replace with None return None - val: float | int = scale * float(entry) + offset + val: float | int = scale * entry + offset if self._min_value is not None and val < self._min_value: val = self._min_value if self._max_value is not None and val > self._max_value: @@ -247,7 +247,7 @@ def __process_raw_value( return f"{float(val):.{self._precision}f}" def unpack_structure_result( - self, registers: list[int], scale: float, offset: float + self, registers: list[int], scale: float = 1, offset: float = 0 ) -> str | None: """Convert registers to proper result.""" diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index f8f1a7450eba8..ed66522b6619f 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -15,6 +15,7 @@ CONF_COUNT, CONF_HOST, CONF_NAME, + CONF_OFFSET, CONF_PORT, CONF_SCAN_INTERVAL, CONF_STRUCTURE, @@ -25,16 +26,23 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import ( + CONF_CURRENT_TEMP_OFFSET, + CONF_CURRENT_TEMP_SCALE, CONF_DATA_TYPE, CONF_FAN_MODE_VALUES, + CONF_SCALE, CONF_SLAVE_COUNT, CONF_SWAP, CONF_SWAP_BYTE, CONF_SWAP_WORD, CONF_SWAP_WORD_BYTE, CONF_SWING_MODE_VALUES, + CONF_TARGET_TEMP_OFFSET, + CONF_TARGET_TEMP_SCALE, CONF_VIRTUAL_COUNT, DEFAULT_HUB, + DEFAULT_OFFSET, + DEFAULT_SCALE, DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, PLATFORMS, @@ -243,6 +251,60 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict: return config +def ensure_and_check_conflicting_scales_and_offsets(config: dict[str, Any]) -> dict: + """Check for conflicts in scale/offset and ensure target/current temp scale/offset is set.""" + + if ( + config[CONF_TARGET_TEMP_SCALE] != config[CONF_SCALE] + and config[CONF_SCALE] != DEFAULT_SCALE + and config[CONF_TARGET_TEMP_SCALE] != DEFAULT_SCALE + ): + raise vol.Invalid( + f"Invalid scales: {CONF_SCALE} and {CONF_TARGET_TEMP_SCALE} cannot be used together, please use only one of them." + ) + + if config[CONF_SCALE] != DEFAULT_SCALE: + config[CONF_TARGET_TEMP_SCALE] = config[CONF_SCALE] + + if ( + config[CONF_CURRENT_TEMP_SCALE] != config[CONF_SCALE] + and config[CONF_SCALE] != DEFAULT_SCALE + and config[CONF_CURRENT_TEMP_SCALE] != DEFAULT_SCALE + ): + raise vol.Invalid( + f"Invalid scales: {CONF_SCALE} and {CONF_CURRENT_TEMP_SCALE} cannot be used together, please use only one of them." + ) + + if config[CONF_SCALE] != DEFAULT_SCALE: + config[CONF_CURRENT_TEMP_SCALE] = config[CONF_SCALE] + + if ( + config[CONF_TARGET_TEMP_OFFSET] != config[CONF_OFFSET] + and config[CONF_OFFSET] != DEFAULT_OFFSET + and config[CONF_TARGET_TEMP_OFFSET] != DEFAULT_OFFSET + ): + raise vol.Invalid( + f"Invalid scales: {CONF_OFFSET} and {CONF_TARGET_TEMP_OFFSET} cannot be used together, please use only one of them." + ) + + if config[CONF_OFFSET] != DEFAULT_OFFSET: + config[CONF_TARGET_TEMP_OFFSET] = config[CONF_OFFSET] + + if ( + config[CONF_CURRENT_TEMP_OFFSET] != config[CONF_OFFSET] + and config[CONF_OFFSET] != DEFAULT_OFFSET + and config[CONF_CURRENT_TEMP_OFFSET] != DEFAULT_OFFSET + ): + raise vol.Invalid( + f"Invalid scales: {CONF_OFFSET} and {CONF_CURRENT_TEMP_OFFSET} cannot be used together, please use only one of them." + ) + + if config[CONF_OFFSET] != DEFAULT_OFFSET: + config[CONF_CURRENT_TEMP_OFFSET] = config[CONF_OFFSET] + + 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() diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 3bd70cf3fcfbb..46fe06297d6da 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -934,160 +934,6 @@ async def test_hvac_onoff_register_transition_update( assert state.state == result_after -@pytest.mark.parametrize( - ("do_config", "result", "register_words"), - [ - ( - { - CONF_CLIMATES: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_TARGET_TEMP: 120, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 0, - CONF_DATA_TYPE: DataType.INT32, - }, - ] - }, - 17, - [0, 17], - ), - ( - { - CONF_CLIMATES: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_TARGET_TEMP: 120, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 0, - CONF_CURRENT_TEMP_SCALE: 0.01, - CONF_CURRENT_TEMP_OFFSET: 10, - CONF_SCALE: 10, - CONF_OFFSET: 20, - }, - ] - }, - 35, - [2500], - ), - ( - { - CONF_CLIMATES: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_TARGET_TEMP: 120, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 0, - CONF_CURRENT_TEMP_SCALE: 0.01, - CONF_CURRENT_TEMP_OFFSET: 10, - CONF_SCALE: 10, - CONF_OFFSET: -20, - }, - ] - }, - 29, - [1900], - ), - ], -) -async def test_config_current_temp_scale_and_offset( - hass: HomeAssistant, mock_modbus_ha, result, register_words -) -> None: - """Test behavior with different configurations for temperature scaling.""" - mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) - - await hass.services.async_call( - HOMEASSISTANT_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: ENTITY_ID}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_ID) - assert state.attributes.get("current_temperature") == result - - -@pytest.mark.parametrize( - ("do_config", "result", "register_words"), - [ - ( - { - CONF_CLIMATES: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_TARGET_TEMP: 120, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 0, - CONF_DATA_TYPE: DataType.INT32, - }, - ] - }, - 17, - [0, 17], - ), - ( - { - CONF_CLIMATES: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_TARGET_TEMP: 120, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 0, - CONF_TARGET_TEMP_SCALE: 0.01, - CONF_TARGET_TEMP_OFFSET: -5, - CONF_SCALE: 10, - CONF_OFFSET: 20, - }, - ] - }, - 20, - [2500], - ), - ( - { - CONF_CLIMATES: [ - { - CONF_NAME: TEST_ENTITY_NAME, - CONF_TARGET_TEMP: 120, - CONF_ADDRESS: 117, - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 0, - CONF_TARGET_TEMP_SCALE: 0.1, - CONF_TARGET_TEMP_OFFSET: 5, - CONF_SCALE: 10, - CONF_OFFSET: -20, - }, - ] - }, - 26, - [210], - ), - ], -) -async def test_config_target_temp_scale_and_offset( - hass: HomeAssistant, mock_modbus_ha, result, register_words -) -> None: - """Test behavior with different configurations for temperature scaling.""" - mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) - - await hass.services.async_call( - HOMEASSISTANT_DOMAIN, - SERVICE_UPDATE_ENTITY, - {ATTR_ENTITY_ID: ENTITY_ID}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_ID) - assert state.attributes.get("temperature") == result - - @pytest.mark.parametrize( ("do_config", "result", "register_words"), [ @@ -1859,3 +1705,149 @@ async def test_no_discovery_info_climate( ) await hass.async_block_till_done() assert CLIMATE_DOMAIN in hass.config.components + + +@pytest.mark.parametrize( + ("do_config", "result", "register_words"), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + }, + ] + }, + 17, + [17], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_SCALE: 10, + CONF_OFFSET: 20, + }, + ] + }, + 30, + [1], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_CURRENT_TEMP_SCALE: 2, + CONF_CURRENT_TEMP_OFFSET: 10, + }, + ] + }, + 30, + [10], + ), + ], +) +async def test_update_current_temp_scale_and_offset( + hass: HomeAssistant, mock_modbus_ha, result, register_words +) -> None: + """Test behavior with different configurations for temperature scaling.""" + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes.get("current_temperature") == result + + +@pytest.mark.parametrize( + ("do_config", "result", "register_words"), + [ + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + }, + ] + }, + 17, + [17], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_TARGET_TEMP_SCALE: 1, + CONF_TARGET_TEMP_OFFSET: 0, + CONF_SCALE: 10, + CONF_OFFSET: 20, + }, + ] + }, + 120, + [10], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_TARGET_TEMP_SCALE: 0.1, + CONF_TARGET_TEMP_OFFSET: 5, + }, + ] + }, + 26, + [210], + ), + ], +) +async def test_update_target_temp_scale_and_offset( + hass: HomeAssistant, mock_modbus_ha, result, register_words +) -> None: + """Test behavior with different configurations for temperature scaling.""" + mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) + + await hass.services.async_call( + HOMEASSISTANT_DOMAIN, + SERVICE_UPDATE_ENTITY, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state.attributes.get("temperature") == result diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 3816e9878cb4c..e9af404d2141e 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -42,6 +42,8 @@ CONF_BAUDRATE, CONF_BYTESIZE, CONF_CLIMATES, + CONF_CURRENT_TEMP_OFFSET, + CONF_CURRENT_TEMP_SCALE, CONF_DATA_TYPE, CONF_DEVICE_ADDRESS, CONF_FAN_MODE_HIGH, @@ -51,6 +53,7 @@ CONF_INPUT_TYPE, CONF_MSG_WAIT, CONF_PARITY, + CONF_SCALE, CONF_SLAVE_COUNT, CONF_STOPBITS, CONF_SWAP, @@ -61,6 +64,9 @@ CONF_SWING_MODE_SWING_OFF, CONF_SWING_MODE_SWING_ON, CONF_SWING_MODE_VALUES, + CONF_TARGET_TEMP, + CONF_TARGET_TEMP_OFFSET, + CONF_TARGET_TEMP_SCALE, CONF_VIRTUAL_COUNT, DEFAULT_SCAN_INTERVAL, DEVICE_ID, @@ -78,6 +84,7 @@ check_config, duplicate_fan_mode_validator, duplicate_swing_mode_validator, + ensure_and_check_conflicting_scales_and_offsets, hvac_fixedsize_reglist_validator, nan_validator, register_int_list_validator, @@ -93,6 +100,7 @@ CONF_HOST, CONF_METHOD, CONF_NAME, + CONF_OFFSET, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, @@ -1423,3 +1431,75 @@ async def test_pb_service_write_no_slave( if do_return[DATA]: assert any(message.startswith("Pymodbus:") for message in caplog.messages) + + +@pytest.mark.parametrize( + "do_config", + [ + ( + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_TARGET_TEMP_SCALE: 2, + CONF_TARGET_TEMP_OFFSET: 0, + CONF_SCALE: 10, + CONF_OFFSET: 20, + CONF_CURRENT_TEMP_SCALE: 1, + CONF_CURRENT_TEMP_OFFSET: 0, + }, + ), + ( + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_TARGET_TEMP_SCALE: 1, + CONF_TARGET_TEMP_OFFSET: 10, + CONF_SCALE: 1, + CONF_OFFSET: 20, + CONF_CURRENT_TEMP_SCALE: 1, + CONF_CURRENT_TEMP_OFFSET: 0, + }, + ), + ( + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_CURRENT_TEMP_SCALE: 0.1, + CONF_CURRENT_TEMP_OFFSET: 0, + CONF_SCALE: 10, + CONF_OFFSET: 20, + CONF_TARGET_TEMP_SCALE: 1, + CONF_TARGET_TEMP_OFFSET: 0, + }, + ), + ( + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_CURRENT_TEMP_SCALE: 1, + CONF_CURRENT_TEMP_OFFSET: 5, + CONF_SCALE: 10, + CONF_OFFSET: 20, + CONF_TARGET_TEMP_SCALE: 1, + CONF_TARGET_TEMP_OFFSET: 0, + }, + ), + ], +) +async def test_ensure_and_check_conflicting_scales_and_offsets(do_config) -> None: + """Test ensure_and_check_conflicting_scales_and_offsets.""" + + with pytest.raises(vol.Invalid): + ensure_and_check_conflicting_scales_and_offsets(do_config[0]) From 33fec8dbaa2ccd46b7bb54d93013d9251c3e0a56 Mon Sep 17 00:00:00 2001 From: crug80 Date: Mon, 8 Sep 2025 20:24:32 +0000 Subject: [PATCH 07/13] floats --- homeassistant/components/modbus/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 008f3472dc2b5..ea41867e298f2 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -209,7 +209,7 @@ def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: def __process_raw_value( self, entry: float | bytes, - scale: float = 1, + scale: float = 1.0, offset: float = 0, ) -> str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" @@ -232,7 +232,7 @@ def __process_raw_value( return f"{float(val):.{self._precision}f}" def unpack_structure_result( - self, registers: list[int], scale: float = 1, offset: float = 0 + self, registers: list[int], scale: float = 1.0, offset: float = 0 ) -> str | None: """Convert registers to proper result.""" From a8da7180273ca947cc5946e4f22fdbcb45d58747 Mon Sep 17 00:00:00 2001 From: crug80 Date: Tue, 9 Sep 2025 08:31:07 +0000 Subject: [PATCH 08/13] fix requests --- homeassistant/components/modbus/__init__.py | 22 +++----- homeassistant/components/modbus/climate.py | 14 +++-- homeassistant/components/modbus/entity.py | 6 --- homeassistant/components/modbus/sensor.py | 14 ++++- homeassistant/components/modbus/validators.py | 53 ++++++++----------- tests/components/modbus/test_climate.py | 4 +- tests/components/modbus/test_init.py | 16 ++---- 7 files changed, 54 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 24a06eb8583d5..1dcf354e72005 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -150,8 +150,6 @@ DEFAULT_HUB, DEFAULT_HVAC_OFF_VALUE, DEFAULT_HVAC_ON_VALUE, - DEFAULT_OFFSET, - DEFAULT_SCALE, DEFAULT_SCAN_INTERVAL, DEFAULT_TEMP_UNIT, MODBUS_DOMAIN as DOMAIN, @@ -217,8 +215,8 @@ ] ), vol.Optional(CONF_STRUCTURE): cv.string, - vol.Optional(CONF_SCALE, default=DEFAULT_SCALE): vol.Coerce(float), - vol.Optional(CONF_OFFSET, default=DEFAULT_OFFSET): vol.Coerce(float), + vol.Optional(CONF_SCALE): vol.Coerce(float), + vol.Optional(CONF_OFFSET): vol.Coerce(float), vol.Optional(CONF_PRECISION): cv.positive_int, vol.Optional( CONF_SWAP, @@ -280,18 +278,10 @@ vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int, vol.Exclusive(CONF_HVAC_ONOFF_REGISTER, "hvac_onoff_type"): cv.positive_int, - vol.Optional(CONF_CURRENT_TEMP_SCALE, default=DEFAULT_SCALE): vol.Coerce( - float - ), - vol.Optional(CONF_TARGET_TEMP_SCALE, default=DEFAULT_SCALE): vol.Coerce( - float - ), - vol.Optional(CONF_CURRENT_TEMP_OFFSET, default=DEFAULT_OFFSET): vol.Coerce( - float - ), - vol.Optional(CONF_TARGET_TEMP_OFFSET, default=DEFAULT_OFFSET): vol.Coerce( - float - ), + vol.Optional(CONF_CURRENT_TEMP_SCALE): vol.Coerce(float), + vol.Optional(CONF_TARGET_TEMP_SCALE): vol.Coerce(float), + vol.Optional(CONF_CURRENT_TEMP_OFFSET): vol.Coerce(float), + vol.Optional(CONF_TARGET_TEMP_OFFSET): vol.Coerce(float), vol.Optional( CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE ): cv.positive_int, diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 7e98cd4b675b5..e603a71b6bfd8 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -494,7 +494,7 @@ async def _async_update(self) -> None: # Read the HVAC mode register if defined if self._hvac_mode_register is not None: hvac_mode = await self._async_read_register( - CALL_TYPE_REGISTER_HOLDING, self._hvac_mode_register, raw=True + CALL_TYPE_REGISTER_HOLDING, self._hvac_mode_register, 1.0, 0, raw=True ) # Translate the value received @@ -513,7 +513,7 @@ async def _async_update(self) -> None: # Read the HVAC action register if defined if self._hvac_action_register is not None: hvac_action = await self._async_read_register( - self._hvac_action_type, self._hvac_action_register, raw=True + self._hvac_action_type, self._hvac_action_register, 1.0, 0, raw=True ) # Translate the value received @@ -531,6 +531,8 @@ async def _async_update(self) -> None: self._fan_mode_register if isinstance(self._fan_mode_register, int) else self._fan_mode_register[0], + 1.0, + 0, raw=True, ) @@ -547,6 +549,8 @@ async def _async_update(self) -> None: self._swing_mode_register if isinstance(self._swing_mode_register, int) else self._swing_mode_register[0], + 1.0, + 0, raw=True, ) @@ -565,7 +569,7 @@ async def _async_update(self) -> None: # in the mode register. if self._hvac_onoff_register is not None: onoff = await self._async_read_register( - CALL_TYPE_REGISTER_HOLDING, self._hvac_onoff_register, raw=True + CALL_TYPE_REGISTER_HOLDING, self._hvac_onoff_register, 1.0, 0, raw=True ) if onoff == self._hvac_off_value: self._attr_hvac_mode = HVACMode.OFF @@ -579,8 +583,8 @@ async def _async_read_register( self, register_type: str, register: int, - scale: float = 1, - offset: float = 0, + scale: float, + offset: float, raw: bool | None = False, ) -> float | None: """Read register using the Modbus hub slave.""" diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index ea41867e298f2..233e77d61a78d 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -17,7 +17,6 @@ CONF_DELAY, CONF_DEVICE_CLASS, CONF_NAME, - CONF_OFFSET, CONF_SCAN_INTERVAL, CONF_SLAVE, CONF_STRUCTURE, @@ -50,7 +49,6 @@ CONF_MIN_VALUE, CONF_NAN_VALUE, CONF_PRECISION, - CONF_SCALE, CONF_SLAVE_COUNT, CONF_STATE_OFF, CONF_STATE_ON, @@ -163,8 +161,6 @@ def __init__(self, hass: HomeAssistant, hub: ModbusHub, config: dict) -> None: self._swap = config[CONF_SWAP] self._data_type = config[CONF_DATA_TYPE] self._structure: str = config[CONF_STRUCTURE] - self._scale = config[CONF_SCALE] - self._offset = config[CONF_OFFSET] self._slave_count = config.get(CONF_SLAVE_COUNT) or config.get( CONF_VIRTUAL_COUNT, 0 ) @@ -181,8 +177,6 @@ def __init__(self, hass: HomeAssistant, hub: ModbusHub, config: dict) -> None: self._precision = config.get(CONF_PRECISION, 2) else: self._precision = config.get(CONF_PRECISION, 0) - if self._precision > 0 or self._scale != int(self._scale): - self._value_is_int = False def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: """Do swap as needed.""" diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index d0a7e032e1aa1..083474f5b5601 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_DEVICE_CLASS, CONF_NAME, + CONF_OFFSET, CONF_SENSORS, CONF_UNIQUE_ID, CONF_UNIT_OF_MEASUREMENT, @@ -25,7 +26,14 @@ ) from . import get_hub -from .const import _LOGGER, CONF_SLAVE_COUNT, CONF_VIRTUAL_COUNT +from .const import ( + _LOGGER, + CONF_SCALE, + CONF_SLAVE_COUNT, + CONF_VIRTUAL_COUNT, + DEFAULT_OFFSET, + DEFAULT_SCALE, +) from .entity import BaseStructPlatform from .modbus import ModbusHub @@ -73,9 +81,13 @@ def __init__( self._coordinator: DataUpdateCoordinator[list[float | None] | None] | None = ( None ) + self._scale = entry.get(CONF_SCALE, DEFAULT_SCALE) + self._offset = entry.get(CONF_OFFSET, DEFAULT_OFFSET) self._attr_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT) self._attr_state_class = entry.get(CONF_STATE_CLASS) self._attr_device_class = entry.get(CONF_DEVICE_CLASS) + if self._precision > 0 or self._scale != int(self._scale): + self._value_is_int = False async def async_setup_slaves( self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any] diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index ed66522b6619f..34e2b07bfc2d0 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -254,53 +254,44 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict: def ensure_and_check_conflicting_scales_and_offsets(config: dict[str, Any]) -> dict: """Check for conflicts in scale/offset and ensure target/current temp scale/offset is set.""" - if ( - config[CONF_TARGET_TEMP_SCALE] != config[CONF_SCALE] - and config[CONF_SCALE] != DEFAULT_SCALE - and config[CONF_TARGET_TEMP_SCALE] != DEFAULT_SCALE - ): + if CONF_TARGET_TEMP_SCALE not in config: + config[CONF_TARGET_TEMP_SCALE] = config.get(CONF_SCALE, DEFAULT_SCALE) + elif CONF_SCALE in config and config[CONF_TARGET_TEMP_SCALE] != config[CONF_SCALE]: raise vol.Invalid( f"Invalid scales: {CONF_SCALE} and {CONF_TARGET_TEMP_SCALE} cannot be used together, please use only one of them." ) - if config[CONF_SCALE] != DEFAULT_SCALE: - config[CONF_TARGET_TEMP_SCALE] = config[CONF_SCALE] - - if ( - config[CONF_CURRENT_TEMP_SCALE] != config[CONF_SCALE] - and config[CONF_SCALE] != DEFAULT_SCALE - and config[CONF_CURRENT_TEMP_SCALE] != DEFAULT_SCALE - ): + if CONF_CURRENT_TEMP_SCALE not in config: + config[CONF_CURRENT_TEMP_SCALE] = config.get(CONF_SCALE, DEFAULT_SCALE) + elif CONF_SCALE in config and config[CONF_CURRENT_TEMP_SCALE] != config[CONF_SCALE]: raise vol.Invalid( f"Invalid scales: {CONF_SCALE} and {CONF_CURRENT_TEMP_SCALE} cannot be used together, please use only one of them." ) - if config[CONF_SCALE] != DEFAULT_SCALE: - config[CONF_CURRENT_TEMP_SCALE] = config[CONF_SCALE] - - if ( - config[CONF_TARGET_TEMP_OFFSET] != config[CONF_OFFSET] - and config[CONF_OFFSET] != DEFAULT_OFFSET - and config[CONF_TARGET_TEMP_OFFSET] != DEFAULT_OFFSET + if CONF_TARGET_TEMP_OFFSET not in config: + config[CONF_TARGET_TEMP_OFFSET] = config.get(CONF_OFFSET, DEFAULT_OFFSET) + elif ( + CONF_OFFSET in config and config[CONF_TARGET_TEMP_OFFSET] != config[CONF_OFFSET] ): raise vol.Invalid( - f"Invalid scales: {CONF_OFFSET} and {CONF_TARGET_TEMP_OFFSET} cannot be used together, please use only one of them." + f"Invalid offsets: {CONF_OFFSET} and {CONF_TARGET_TEMP_OFFSET} cannot be used together, please use only one of them." ) - if config[CONF_OFFSET] != DEFAULT_OFFSET: - config[CONF_TARGET_TEMP_OFFSET] = config[CONF_OFFSET] - - if ( - config[CONF_CURRENT_TEMP_OFFSET] != config[CONF_OFFSET] - and config[CONF_OFFSET] != DEFAULT_OFFSET - and config[CONF_CURRENT_TEMP_OFFSET] != DEFAULT_OFFSET + if CONF_CURRENT_TEMP_OFFSET not in config: + config[CONF_CURRENT_TEMP_OFFSET] = config.get(CONF_OFFSET, DEFAULT_OFFSET) + elif ( + CONF_OFFSET in config + and config[CONF_CURRENT_TEMP_OFFSET] != config[CONF_OFFSET] ): raise vol.Invalid( - f"Invalid scales: {CONF_OFFSET} and {CONF_CURRENT_TEMP_OFFSET} cannot be used together, please use only one of them." + f"Invalid offsets: {CONF_OFFSET} and {CONF_CURRENT_TEMP_OFFSET} cannot be used together, please use only one of them." ) - if config[CONF_OFFSET] != DEFAULT_OFFSET: - config[CONF_CURRENT_TEMP_OFFSET] = config[CONF_OFFSET] + if CONF_OFFSET in config: + del config[CONF_OFFSET] + + if CONF_SCALE in config: + del config[CONF_SCALE] return config diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 46fe06297d6da..b1169ebafb56d 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -1808,12 +1808,10 @@ async def test_update_current_temp_scale_and_offset( CONF_SCAN_INTERVAL: 0, CONF_TARGET_TEMP_SCALE: 1, CONF_TARGET_TEMP_OFFSET: 0, - CONF_SCALE: 10, - CONF_OFFSET: 20, }, ] }, - 120, + 10, [10], ), ( diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index e9af404d2141e..4808f3891b739 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -1443,8 +1443,6 @@ async def test_pb_service_write_no_slave( CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 0, - CONF_TARGET_TEMP_SCALE: 2, - CONF_TARGET_TEMP_OFFSET: 0, CONF_SCALE: 10, CONF_OFFSET: 20, CONF_CURRENT_TEMP_SCALE: 1, @@ -1458,8 +1456,6 @@ async def test_pb_service_write_no_slave( CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 0, - CONF_TARGET_TEMP_SCALE: 1, - CONF_TARGET_TEMP_OFFSET: 10, CONF_SCALE: 1, CONF_OFFSET: 20, CONF_CURRENT_TEMP_SCALE: 1, @@ -1473,12 +1469,9 @@ async def test_pb_service_write_no_slave( CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 0, - CONF_CURRENT_TEMP_SCALE: 0.1, - CONF_CURRENT_TEMP_OFFSET: 0, - CONF_SCALE: 10, + CONF_SCALE: 1, CONF_OFFSET: 20, - CONF_TARGET_TEMP_SCALE: 1, - CONF_TARGET_TEMP_OFFSET: 0, + CONF_TARGET_TEMP_SCALE: 20, }, ), ( @@ -1488,12 +1481,9 @@ async def test_pb_service_write_no_slave( CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 0, - CONF_CURRENT_TEMP_SCALE: 1, - CONF_CURRENT_TEMP_OFFSET: 5, CONF_SCALE: 10, CONF_OFFSET: 20, - CONF_TARGET_TEMP_SCALE: 1, - CONF_TARGET_TEMP_OFFSET: 0, + CONF_TARGET_TEMP_OFFSET: 30, }, ), ], From 0cd6f6af899736f0ea9195d7018bc6d9d154ba03 Mon Sep 17 00:00:00 2001 From: crug80 Date: Tue, 9 Sep 2025 14:26:29 +0000 Subject: [PATCH 09/13] fix requests 2 --- homeassistant/components/modbus/climate.py | 8 +-- homeassistant/components/modbus/validators.py | 58 +++++++------------ tests/components/modbus/test_climate.py | 8 +-- tests/components/modbus/test_init.py | 2 - 4 files changed, 30 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index e603a71b6bfd8..1b16b373ecef2 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -480,15 +480,15 @@ async def _async_update(self) -> None: self._target_temperature_register[ HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode] ], - scale=self._target_temp_scale, - offset=self._target_temp_offset, + self._target_temp_scale, + self._target_temp_offset, ) self._attr_current_temperature = await self._async_read_register( self._input_type, self._address, - scale=self._current_temp_scale, - offset=self._current_temp_offset, + self._current_temp_scale, + self._current_temp_offset, ) # Read the HVAC mode register if defined diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 34e2b07bfc2d0..e45d8a614c4c5 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -253,45 +253,31 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict: def ensure_and_check_conflicting_scales_and_offsets(config: dict[str, Any]) -> dict: """Check for conflicts in scale/offset and ensure target/current temp scale/offset is set.""" + config_keys = [ + (CONF_SCALE, CONF_TARGET_TEMP_SCALE, CONF_CURRENT_TEMP_SCALE, DEFAULT_SCALE), + ( + CONF_OFFSET, + CONF_TARGET_TEMP_OFFSET, + CONF_CURRENT_TEMP_OFFSET, + DEFAULT_OFFSET, + ), + ] - if CONF_TARGET_TEMP_SCALE not in config: - config[CONF_TARGET_TEMP_SCALE] = config.get(CONF_SCALE, DEFAULT_SCALE) - elif CONF_SCALE in config and config[CONF_TARGET_TEMP_SCALE] != config[CONF_SCALE]: - raise vol.Invalid( - f"Invalid scales: {CONF_SCALE} and {CONF_TARGET_TEMP_SCALE} cannot be used together, please use only one of them." - ) - - if CONF_CURRENT_TEMP_SCALE not in config: - config[CONF_CURRENT_TEMP_SCALE] = config.get(CONF_SCALE, DEFAULT_SCALE) - elif CONF_SCALE in config and config[CONF_CURRENT_TEMP_SCALE] != config[CONF_SCALE]: - raise vol.Invalid( - f"Invalid scales: {CONF_SCALE} and {CONF_CURRENT_TEMP_SCALE} cannot be used together, please use only one of them." - ) - - if CONF_TARGET_TEMP_OFFSET not in config: - config[CONF_TARGET_TEMP_OFFSET] = config.get(CONF_OFFSET, DEFAULT_OFFSET) - elif ( - CONF_OFFSET in config and config[CONF_TARGET_TEMP_OFFSET] != config[CONF_OFFSET] - ): - raise vol.Invalid( - f"Invalid offsets: {CONF_OFFSET} and {CONF_TARGET_TEMP_OFFSET} cannot be used together, please use only one of them." - ) - - if CONF_CURRENT_TEMP_OFFSET not in config: - config[CONF_CURRENT_TEMP_OFFSET] = config.get(CONF_OFFSET, DEFAULT_OFFSET) - elif ( - CONF_OFFSET in config - and config[CONF_CURRENT_TEMP_OFFSET] != config[CONF_OFFSET] - ): - raise vol.Invalid( - f"Invalid offsets: {CONF_OFFSET} and {CONF_CURRENT_TEMP_OFFSET} cannot be used together, please use only one of them." - ) + for generic_key, target_key, current_key, default_value in config_keys: + if generic_key in config and (target_key in config or current_key in config): + raise vol.Invalid( + f"Cannot use {target_key} or {current_key} with {generic_key} in the same configuration." + ) - if CONF_OFFSET in config: - del config[CONF_OFFSET] + if generic_key in config: + value = config.pop(generic_key) + config[target_key] = value + config[current_key] = value - if CONF_SCALE in config: - del config[CONF_SCALE] + if target_key not in config or config[target_key] == 0: + config[target_key] = default_value + if current_key not in config or config[current_key] == 0: + config[current_key] = default_value return config diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index b1169ebafb56d..820638f4e274f 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -1764,7 +1764,7 @@ async def test_no_discovery_info_climate( async def test_update_current_temp_scale_and_offset( hass: HomeAssistant, mock_modbus_ha, result, register_words ) -> None: - """Test behavior with different configurations for temperature scaling.""" + """Test behavior with different configurations for current temperature scaling/offset.""" mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) await hass.services.async_call( @@ -1823,8 +1823,8 @@ async def test_update_current_temp_scale_and_offset( CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 0, - CONF_TARGET_TEMP_SCALE: 0.1, - CONF_TARGET_TEMP_OFFSET: 5, + CONF_SCALE: 0.1, + CONF_OFFSET: 5, }, ] }, @@ -1836,7 +1836,7 @@ async def test_update_current_temp_scale_and_offset( async def test_update_target_temp_scale_and_offset( hass: HomeAssistant, mock_modbus_ha, result, register_words ) -> None: - """Test behavior with different configurations for temperature scaling.""" + """Test behavior with different configurations for target temperature scaling / offset.""" mock_modbus_ha.read_holding_registers.return_value = ReadResult(register_words) await hass.services.async_call( diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 4808f3891b739..9e81ce0aec50b 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -1446,7 +1446,6 @@ async def test_pb_service_write_no_slave( CONF_SCALE: 10, CONF_OFFSET: 20, CONF_CURRENT_TEMP_SCALE: 1, - CONF_CURRENT_TEMP_OFFSET: 0, }, ), ( @@ -1458,7 +1457,6 @@ async def test_pb_service_write_no_slave( CONF_SCAN_INTERVAL: 0, CONF_SCALE: 1, CONF_OFFSET: 20, - CONF_CURRENT_TEMP_SCALE: 1, CONF_CURRENT_TEMP_OFFSET: 0, }, ), From c9ae098b352dd575086bc0370008e1fce276ca87 Mon Sep 17 00:00:00 2001 From: crug80 Date: Sat, 13 Sep 2025 14:17:25 +0000 Subject: [PATCH 10/13] fix requests 3 --- homeassistant/components/modbus/climate.py | 28 +++++++++--- homeassistant/components/modbus/entity.py | 11 +++-- tests/components/modbus/test_climate.py | 51 ++++++++++++++++++++++ 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index 1b16b373ecef2..ad4c540a26b6d 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -103,6 +103,8 @@ CONF_TARGET_TEMP_SCALE, CONF_TARGET_TEMP_WRITE_REGISTERS, CONF_WRITE_REGISTERS, + DEFAULT_OFFSET, + DEFAULT_SCALE, DataType, ) from .entity import BaseStructPlatform @@ -494,7 +496,11 @@ async def _async_update(self) -> None: # Read the HVAC mode register if defined if self._hvac_mode_register is not None: hvac_mode = await self._async_read_register( - CALL_TYPE_REGISTER_HOLDING, self._hvac_mode_register, 1.0, 0, raw=True + CALL_TYPE_REGISTER_HOLDING, + self._hvac_mode_register, + DEFAULT_SCALE, + DEFAULT_OFFSET, + raw=True, ) # Translate the value received @@ -513,7 +519,11 @@ async def _async_update(self) -> None: # Read the HVAC action register if defined if self._hvac_action_register is not None: hvac_action = await self._async_read_register( - self._hvac_action_type, self._hvac_action_register, 1.0, 0, raw=True + self._hvac_action_type, + self._hvac_action_register, + DEFAULT_SCALE, + DEFAULT_OFFSET, + raw=True, ) # Translate the value received @@ -531,8 +541,8 @@ async def _async_update(self) -> None: self._fan_mode_register if isinstance(self._fan_mode_register, int) else self._fan_mode_register[0], - 1.0, - 0, + DEFAULT_SCALE, + DEFAULT_OFFSET, raw=True, ) @@ -549,8 +559,8 @@ async def _async_update(self) -> None: self._swing_mode_register if isinstance(self._swing_mode_register, int) else self._swing_mode_register[0], - 1.0, - 0, + DEFAULT_SCALE, + DEFAULT_OFFSET, raw=True, ) @@ -569,7 +579,11 @@ async def _async_update(self) -> None: # in the mode register. if self._hvac_onoff_register is not None: onoff = await self._async_read_register( - CALL_TYPE_REGISTER_HOLDING, self._hvac_onoff_register, 1.0, 0, raw=True + CALL_TYPE_REGISTER_HOLDING, + self._hvac_onoff_register, + DEFAULT_SCALE, + DEFAULT_OFFSET, + raw=True, ) if onoff == self._hvac_off_value: self._attr_hvac_mode = HVACMode.OFF diff --git a/homeassistant/components/modbus/entity.py b/homeassistant/components/modbus/entity.py index 233e77d61a78d..5264cce32e44d 100644 --- a/homeassistant/components/modbus/entity.py +++ b/homeassistant/components/modbus/entity.py @@ -60,6 +60,8 @@ CONF_VIRTUAL_COUNT, CONF_WRITE_TYPE, CONF_ZERO_SUPPRESS, + DEFAULT_OFFSET, + DEFAULT_SCALE, SIGNAL_STOP_ENTITY, DataType, ) @@ -203,8 +205,8 @@ def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]: def __process_raw_value( self, entry: float | bytes, - scale: float = 1.0, - offset: float = 0, + scale: float = DEFAULT_SCALE, + offset: float = DEFAULT_OFFSET, ) -> str | None: """Process value from sensor with NaN handling, scaling, offset, min/max etc.""" if self._nan_value and entry in (self._nan_value, -self._nan_value): @@ -226,7 +228,10 @@ def __process_raw_value( return f"{float(val):.{self._precision}f}" def unpack_structure_result( - self, registers: list[int], scale: float = 1.0, offset: float = 0 + self, + registers: list[int], + scale: float = DEFAULT_SCALE, + offset: float = DEFAULT_OFFSET, ) -> str | None: """Convert registers to proper result.""" diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index 820638f4e274f..df4d16f21ffc0 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -1759,6 +1759,23 @@ async def test_no_discovery_info_climate( 30, [10], ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_CURRENT_TEMP_SCALE: 0, + CONF_CURRENT_TEMP_OFFSET: 10, + }, + ] + }, + 20, + [10], + ), ], ) async def test_update_current_temp_scale_and_offset( @@ -1831,6 +1848,40 @@ async def test_update_current_temp_scale_and_offset( 26, [210], ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_SCALE: 0, + CONF_OFFSET: 2, + }, + ] + }, + 12, + [10], + ), + ( + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_TARGET_TEMP_SCALE: 0, + CONF_TARGET_TEMP_OFFSET: 2, + }, + ] + }, + 12, + [10], + ), ], ) async def test_update_target_temp_scale_and_offset( From 2b650a21efe8b76e66f65118127b4335733ef37c Mon Sep 17 00:00:00 2001 From: crug80 Date: Mon, 22 Sep 2025 10:56:08 +0000 Subject: [PATCH 11/13] fix ruff --- homeassistant/components/modbus/sensor.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index 35bda05d90636..2cdb0f33d78a0 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -26,7 +26,6 @@ ) from . import get_hub - from .const import ( _LOGGER, CONF_SCALE, @@ -35,7 +34,6 @@ DEFAULT_OFFSET, DEFAULT_SCALE, ) - from .entity import ModbusStructEntity from .modbus import ModbusHub From bea9139d1274f3e191ec049484f688326d1634b6 Mon Sep 17 00:00:00 2001 From: Claudio Ruggeri - CR-Tech <41435902+crug80@users.noreply.github.com> Date: Mon, 20 Oct 2025 08:18:31 +0200 Subject: [PATCH 12/13] Update homeassistant/components/modbus/validators.py Co-authored-by: Franck Nijhof --- homeassistant/components/modbus/validators.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 388f2600fff58..620d0d261e06e 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -266,9 +266,11 @@ def ensure_and_check_conflicting_scales_and_offsets(config: dict[str, Any]) -> d for generic_key, target_key, current_key, default_value in config_keys: if generic_key in config and (target_key in config or current_key in config): raise vol.Invalid( - f"Cannot use {target_key} or {current_key} with {generic_key} in the same configuration." + f"Cannot use both '{generic_key}' and temperature-specific parameters " + f"('{target_key}' or '{current_key}') in the same configuration. " + f"Either the '{generic_key}' parameter (which applies to both temperatures) " + "or the new temperature-specific parameters, but not both." ) - if generic_key in config: value = config.pop(generic_key) config[target_key] = value From 3496c7c4280b191040001da3d068266625d1bc64 Mon Sep 17 00:00:00 2001 From: crug80 Date: Thu, 30 Oct 2025 22:13:57 +0000 Subject: [PATCH 13/13] fix emontnemery request --- homeassistant/components/modbus/__init__.py | 17 +++++++-- homeassistant/components/modbus/validators.py | 11 +++++- tests/components/modbus/conftest.py | 37 +++++++++++++++++++ tests/components/modbus/test_climate.py | 31 ++++++++++++++-- tests/components/modbus/test_init.py | 8 ++++ tests/components/modbus/test_sensor.py | 22 +++++++++++ 6 files changed, 118 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 3525675343d4c..b32b7de1a919d 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -166,6 +166,7 @@ ensure_and_check_conflicting_scales_and_offsets, hvac_fixedsize_reglist_validator, nan_validator, + not_zero_value, register_int_list_validator, struct_validator, ) @@ -215,7 +216,9 @@ ] ), vol.Optional(CONF_STRUCTURE): cv.string, - vol.Optional(CONF_SCALE): vol.Coerce(float), + vol.Optional(CONF_SCALE): vol.All( + vol.Coerce(float), lambda v: not_zero_value(v, "Scale cannot be zero.") + ), vol.Optional(CONF_OFFSET): vol.Coerce(float), vol.Optional(CONF_PRECISION): cv.positive_int, vol.Optional( @@ -278,8 +281,16 @@ vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string, vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int, vol.Exclusive(CONF_HVAC_ONOFF_REGISTER, "hvac_onoff_type"): cv.positive_int, - vol.Optional(CONF_CURRENT_TEMP_SCALE): vol.Coerce(float), - vol.Optional(CONF_TARGET_TEMP_SCALE): vol.Coerce(float), + vol.Optional(CONF_CURRENT_TEMP_SCALE): vol.All( + vol.Coerce(float), + lambda v: not_zero_value( + v, "Current temperature scale cannot be zero." + ), + ), + vol.Optional(CONF_TARGET_TEMP_SCALE): vol.All( + vol.Coerce(float), + lambda v: not_zero_value(v, "Target temperature scale cannot be zero."), + ), vol.Optional(CONF_CURRENT_TEMP_OFFSET): vol.Coerce(float), vol.Optional(CONF_TARGET_TEMP_OFFSET): vol.Coerce(float), vol.Optional( diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 620d0d261e06e..a9e246c2c9618 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -251,6 +251,13 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict: return config +def not_zero_value(val: float, errMsg: str) -> float: + """Check value is not zero.""" + if val == 0: + raise vol.Invalid(errMsg) + return val + + def ensure_and_check_conflicting_scales_and_offsets(config: dict[str, Any]) -> dict: """Check for conflicts in scale/offset and ensure target/current temp scale/offset is set.""" config_keys = [ @@ -276,9 +283,9 @@ def ensure_and_check_conflicting_scales_and_offsets(config: dict[str, Any]) -> d config[target_key] = value config[current_key] = value - if target_key not in config or config[target_key] == 0: + if target_key not in config: config[target_key] = default_value - if current_key not in config or config[current_key] == 0: + if current_key not in config: config[current_key] = default_value return config diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index a57c2cfdcc586..050100882840b 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -172,6 +172,43 @@ async def mock_modbus_fixture( return mock_pymodbus +@pytest.fixture(name="mock_modbus_to_test_errors_config") +async def mock_modbus_to_test_errors_config_fixture( + hass: HomeAssistant, + check_config_loaded, + config_addon, + do_config, + mock_pymodbus, +): + """Load integration a base hub modbus.""" + conf = copy.deepcopy(do_config) + for key in conf: + if config_addon: + conf[key][0].update(config_addon) + + config = { + DOMAIN: [ + { + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_NAME: TEST_MODBUS_NAME, + **conf, + } + ] + } + now = dt_util.utcnow() + with mock.patch( + "homeassistant.helpers.event.dt_util.utcnow", + return_value=now, + autospec=True, + ): + result = await async_setup_component(hass, DOMAIN, config) + + await hass.async_block_till_done() + return result + + @pytest.fixture(name="mock_do_cycle") async def mock_do_cycle_fixture( hass: HomeAssistant, diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py index af084710ad30a..05206ba15f58c 100644 --- a/tests/components/modbus/test_climate.py +++ b/tests/components/modbus/test_climate.py @@ -1768,7 +1768,7 @@ async def test_no_discovery_info_climate( CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 0, - CONF_CURRENT_TEMP_SCALE: 0, + CONF_CURRENT_TEMP_SCALE: 1, CONF_CURRENT_TEMP_OFFSET: 10, }, ] @@ -1857,7 +1857,7 @@ async def test_update_current_temp_scale_and_offset( CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 0, - CONF_SCALE: 0, + CONF_SCALE: 1, CONF_OFFSET: 2, }, ] @@ -1874,7 +1874,7 @@ async def test_update_current_temp_scale_and_offset( CONF_ADDRESS: 117, CONF_SLAVE: 10, CONF_SCAN_INTERVAL: 0, - CONF_TARGET_TEMP_SCALE: 0, + CONF_TARGET_TEMP_SCALE: 1, CONF_TARGET_TEMP_OFFSET: 2, }, ] @@ -1900,3 +1900,28 @@ async def test_update_target_temp_scale_and_offset( state = hass.states.get(ENTITY_ID) assert state.attributes.get("temperature") == result + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_CLIMATES: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_TARGET_TEMP: 120, + CONF_ADDRESS: 117, + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 0, + CONF_TARGET_TEMP_SCALE: 0, + CONF_TARGET_TEMP_OFFSET: 2, + } + ] + }, + ], +) +async def test_err_config_climate( + hass: HomeAssistant, mock_modbus_to_test_errors_config +) -> None: + """Run a wrong configuration test for climate.""" + assert CLIMATE_DOMAIN not in hass.config.components diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index d05a070fa9b95..c10b4c98cc956 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -87,6 +87,7 @@ ensure_and_check_conflicting_scales_and_offsets, hvac_fixedsize_reglist_validator, nan_validator, + not_zero_value, register_int_list_validator, struct_validator, ) @@ -1624,3 +1625,10 @@ async def test_ensure_and_check_conflicting_scales_and_offsets(do_config) -> Non with pytest.raises(vol.Invalid): ensure_and_check_conflicting_scales_and_offsets(do_config[0]) + + +async def test_not_zero_value() -> None: + """Test not 0 validator validator.""" + + with pytest.raises(vol.Invalid): + not_zero_value(0, "Value cannot be zero.") diff --git a/tests/components/modbus/test_sensor.py b/tests/components/modbus/test_sensor.py index e9e56ff6974d0..89949dfd29ab4 100644 --- a/tests/components/modbus/test_sensor.py +++ b/tests/components/modbus/test_sensor.py @@ -1487,3 +1487,25 @@ async def test_no_discovery_info_sensor( ) await hass.async_block_till_done() assert SENSOR_DOMAIN in hass.config.components + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 51, + CONF_DATA_TYPE: DataType.INT16, + CONF_SCALE: 0, + } + ] + }, + ], +) +async def test_err_config_sensor( + hass: HomeAssistant, mock_modbus_to_test_errors_config +) -> None: + """Run a wrong configuration test for sensor.""" + assert SENSOR_DOMAIN not in hass.config.components