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:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user