1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-20 02:48:57 +00:00

Add separate scale and offset for current temperature for modbus climates (#150985)

Co-authored-by: jan iversen <jancasacondor@gmail.com>
Co-authored-by: Claudio Ruggeri - CR-Tech <41435902+crug80@users.noreply.github.com>
Co-authored-by: crug80 <claudio@cr-tech.it>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Ілля Піскурьов
2025-11-03 17:51:41 +02:00
committed by GitHub
parent fad217837f
commit 1add999c5a
10 changed files with 517 additions and 23 deletions

View File

@@ -66,6 +66,8 @@ from .const import (
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,
@@ -137,6 +139,8 @@ from .const import (
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,
@@ -159,8 +163,10 @@ from .modbus import DATA_MODBUS_HUBS, ModbusHub, async_modbus_setup
from .validators import (
duplicate_fan_mode_validator,
duplicate_swing_mode_validator,
ensure_and_check_conflicting_scales_and_offsets,
hvac_fixedsize_reglist_validator,
nan_validator,
not_zero_value,
register_int_list_validator,
struct_validator,
)
@@ -210,8 +216,10 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
]
),
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): 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(
CONF_SWAP,
@@ -273,6 +281,18 @@ CLIMATE_SCHEMA = vol.All(
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.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(
CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE
): cv.positive_int,
@@ -385,6 +405,7 @@ CLIMATE_SCHEMA = vol.All(
),
},
),
ensure_and_check_conflicting_scales_and_offsets,
)
COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend(

View File

@@ -50,6 +50,8 @@ from .const import (
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,
@@ -97,8 +99,12 @@ from .const import (
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,
DEFAULT_OFFSET,
DEFAULT_SCALE,
DataType,
)
from .entity import ModbusStructEntity
@@ -166,6 +172,10 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
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[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]
@@ -413,8 +423,8 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
target_temperature = (
float(kwargs[ATTR_TEMPERATURE]) - self._offset
) / self._scale
float(kwargs[ATTR_TEMPERATURE]) - self._target_temp_offset
) / self._target_temp_scale
if self._data_type in (
DataType.INT16,
DataType.INT32,
@@ -472,15 +482,25 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
self._target_temperature_register[
HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode]
],
self._target_temp_scale,
self._target_temp_offset,
)
self._attr_current_temperature = await self._async_read_register(
self._input_type, self._address
self._input_type,
self._address,
self._current_temp_scale,
self._current_temp_offset,
)
# 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,
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)
# Translate the value received
@@ -499,7 +519,11 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
# 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,
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)
# Translate the value received
@@ -517,6 +541,8 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
self._fan_mode_register
if isinstance(self._fan_mode_register, int)
else self._fan_mode_register[0],
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)
@@ -533,6 +559,8 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
self._swing_mode_register
if isinstance(self._swing_mode_register, int)
else self._swing_mode_register[0],
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)
@@ -551,7 +579,11 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
# 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,
DEFAULT_SCALE,
DEFAULT_OFFSET,
raw=True,
)
if onoff == self._hvac_off_value:
self._attr_hvac_mode = HVACMode.OFF
@@ -562,7 +594,12 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
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,
scale: float,
offset: float,
raw: bool | None = False,
) -> float | None:
"""Read register using the Modbus hub slave."""
result = await self._hub.async_pb_call(
@@ -579,7 +616,7 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
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, scale, offset)
if not self._value:
self._attr_available = False
return None

View File

@@ -19,6 +19,8 @@ CONF_BYTESIZE = "bytesize"
CONF_CLIMATES = "climates"
CONF_BRIGHTNESS_REGISTER = "brightness_address"
CONF_COLOR_TEMP_REGISTER = "color_temp_address"
CONF_CURRENT_TEMP_OFFSET = "current_temp_offset"
CONF_CURRENT_TEMP_SCALE = "current_temp_scale"
CONF_DATA_TYPE = "data_type"
CONF_DEVICE_ADDRESS = "device_address"
CONF_FANS = "fans"
@@ -48,6 +50,8 @@ CONF_SWAP_BYTE = "byte"
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"
@@ -181,4 +185,7 @@ LIGHT_MODBUS_SCALE_MIN = 0
LIGHT_MODBUS_SCALE_MAX = 100
LIGHT_MODBUS_INVALID_VALUE = 0xFFFF
DEFAULT_SCALE = 1.0
DEFAULT_OFFSET = 0
_LOGGER = logging.getLogger(__package__)

View File

@@ -17,7 +17,6 @@ from homeassistant.const import (
CONF_DELAY,
CONF_DEVICE_CLASS,
CONF_NAME,
CONF_OFFSET,
CONF_SCAN_INTERVAL,
CONF_SLAVE,
CONF_STRUCTURE,
@@ -50,7 +49,6 @@ from .const import (
CONF_MIN_VALUE,
CONF_NAN_VALUE,
CONF_PRECISION,
CONF_SCALE,
CONF_SLAVE_COUNT,
CONF_STATE_OFF,
CONF_STATE_ON,
@@ -62,6 +60,8 @@ from .const import (
CONF_VIRTUAL_COUNT,
CONF_WRITE_TYPE,
CONF_ZERO_SUPPRESS,
DEFAULT_OFFSET,
DEFAULT_SCALE,
SIGNAL_STOP_ENTITY,
DataType,
)
@@ -163,8 +163,6 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
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 +179,6 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
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."""
@@ -206,7 +202,12 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
registers.reverse()
return registers
def __process_raw_value(self, entry: float | str | bytes) -> str | None:
def __process_raw_value(
self,
entry: float | bytes,
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 is not None and entry in (self._nan_value, -self._nan_value):
return None
@@ -215,7 +216,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
if entry != entry: # noqa: PLR0124
# NaN float detection replace with None
return None
val: float | int = self._scale * entry + self._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:
@@ -226,7 +227,12 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
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],
scale: float = DEFAULT_SCALE,
offset: float = DEFAULT_OFFSET,
) -> str | None:
"""Convert registers to proper result."""
if self._swap:
@@ -250,7 +256,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
# 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, scale, offset)
if self._data_type != DataType.CUSTOM:
v_result.append(str(v_temp))
else:
@@ -258,7 +264,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
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], scale, offset)
class ModbusToggleEntity(ModbusBaseEntity, ToggleEntity, RestoreEntity):

View File

@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
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 homeassistant.helpers.update_coordinator import (
)
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 ModbusStructEntity
from .modbus import ModbusHub
@@ -73,9 +81,13 @@ class ModbusRegisterSensor(ModbusStructEntity, RestoreSensor, SensorEntity):
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]
@@ -117,7 +129,9 @@ class ModbusRegisterSensor(ModbusStructEntity, RestoreSensor, SensorEntity):
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:

View File

@@ -15,6 +15,7 @@ from homeassistant.const import (
CONF_COUNT,
CONF_HOST,
CONF_NAME,
CONF_OFFSET,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_STRUCTURE,
@@ -25,16 +26,23 @@ from homeassistant.core import HomeAssistant
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,
DOMAIN,
PLATFORMS,
@@ -243,6 +251,46 @@ 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 = [
(CONF_SCALE, CONF_TARGET_TEMP_SCALE, CONF_CURRENT_TEMP_SCALE, DEFAULT_SCALE),
(
CONF_OFFSET,
CONF_TARGET_TEMP_OFFSET,
CONF_CURRENT_TEMP_OFFSET,
DEFAULT_OFFSET,
),
]
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 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
config[current_key] = value
if target_key not in config:
config[target_key] = default_value
if current_key not in config:
config[current_key] = default_value
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()

View File

@@ -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,

View File

@@ -38,6 +38,8 @@ from homeassistant.components.climate import (
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 @@ from homeassistant.components.modbus.const import (
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,
@@ -82,6 +85,8 @@ from homeassistant.components.modbus.const import (
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,
DOMAIN,
@@ -92,6 +97,7 @@ from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_ADDRESS,
CONF_NAME,
CONF_OFFSET,
CONF_PLATFORM,
CONF_SCAN_INTERVAL,
CONF_SLAVE,
@@ -1699,3 +1705,223 @@ 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],
),
(
{
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: 1,
CONF_CURRENT_TEMP_OFFSET: 10,
},
]
},
20,
[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 current temperature scaling/offset."""
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,
},
]
},
10,
[10],
),
(
{
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 120,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
CONF_SCALE: 0.1,
CONF_OFFSET: 5,
},
]
},
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: 1,
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: 1,
CONF_TARGET_TEMP_OFFSET: 2,
},
]
},
12,
[10],
),
],
)
async def test_update_target_temp_scale_and_offset(
hass: HomeAssistant, mock_modbus_ha, result, register_words
) -> None:
"""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(
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",
[
{
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

View File

@@ -42,6 +42,8 @@ from homeassistant.components.modbus.const import (
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 @@ from homeassistant.components.modbus.const import (
CONF_INPUT_TYPE,
CONF_MSG_WAIT,
CONF_PARITY,
CONF_SCALE,
CONF_SLAVE_COUNT,
CONF_STOPBITS,
CONF_SWAP,
@@ -61,6 +64,9 @@ from homeassistant.components.modbus.const import (
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,8 +84,10 @@ from homeassistant.components.modbus.validators import (
check_config,
duplicate_fan_mode_validator,
duplicate_swing_mode_validator,
ensure_and_check_conflicting_scales_and_offsets,
hvac_fixedsize_reglist_validator,
nan_validator,
not_zero_value,
register_int_list_validator,
struct_validator,
)
@@ -93,6 +101,7 @@ from homeassistant.const import (
CONF_HOST,
CONF_METHOD,
CONF_NAME,
CONF_OFFSET,
CONF_PORT,
CONF_SCAN_INTERVAL,
CONF_SENSORS,
@@ -1556,3 +1565,70 @@ 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_SCALE: 10,
CONF_OFFSET: 20,
CONF_CURRENT_TEMP_SCALE: 1,
},
),
(
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 120,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
CONF_SCALE: 1,
CONF_OFFSET: 20,
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_SCALE: 1,
CONF_OFFSET: 20,
CONF_TARGET_TEMP_SCALE: 20,
},
),
(
{
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,
CONF_TARGET_TEMP_OFFSET: 30,
},
),
],
)
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])
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.")

View File

@@ -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