mirror of
https://github.com/home-assistant/core.git
synced 2025-12-24 12:59:34 +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_BYTESIZE,
|
||||||
CONF_CLIMATES,
|
CONF_CLIMATES,
|
||||||
CONF_COLOR_TEMP_REGISTER,
|
CONF_COLOR_TEMP_REGISTER,
|
||||||
|
CONF_CURRENT_TEMP_OFFSET,
|
||||||
|
CONF_CURRENT_TEMP_SCALE,
|
||||||
CONF_DATA_TYPE,
|
CONF_DATA_TYPE,
|
||||||
CONF_DEVICE_ADDRESS,
|
CONF_DEVICE_ADDRESS,
|
||||||
CONF_FAN_MODE_AUTO,
|
CONF_FAN_MODE_AUTO,
|
||||||
@@ -137,6 +139,8 @@ from .const import (
|
|||||||
CONF_SWING_MODE_SWING_VERT,
|
CONF_SWING_MODE_SWING_VERT,
|
||||||
CONF_SWING_MODE_VALUES,
|
CONF_SWING_MODE_VALUES,
|
||||||
CONF_TARGET_TEMP,
|
CONF_TARGET_TEMP,
|
||||||
|
CONF_TARGET_TEMP_OFFSET,
|
||||||
|
CONF_TARGET_TEMP_SCALE,
|
||||||
CONF_TARGET_TEMP_WRITE_REGISTERS,
|
CONF_TARGET_TEMP_WRITE_REGISTERS,
|
||||||
CONF_VERIFY,
|
CONF_VERIFY,
|
||||||
CONF_VIRTUAL_COUNT,
|
CONF_VIRTUAL_COUNT,
|
||||||
@@ -159,8 +163,10 @@ from .modbus import DATA_MODBUS_HUBS, ModbusHub, async_modbus_setup
|
|||||||
from .validators import (
|
from .validators import (
|
||||||
duplicate_fan_mode_validator,
|
duplicate_fan_mode_validator,
|
||||||
duplicate_swing_mode_validator,
|
duplicate_swing_mode_validator,
|
||||||
|
ensure_and_check_conflicting_scales_and_offsets,
|
||||||
hvac_fixedsize_reglist_validator,
|
hvac_fixedsize_reglist_validator,
|
||||||
nan_validator,
|
nan_validator,
|
||||||
|
not_zero_value,
|
||||||
register_int_list_validator,
|
register_int_list_validator,
|
||||||
struct_validator,
|
struct_validator,
|
||||||
)
|
)
|
||||||
@@ -210,8 +216,10 @@ BASE_STRUCT_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
|
|||||||
]
|
]
|
||||||
),
|
),
|
||||||
vol.Optional(CONF_STRUCTURE): cv.string,
|
vol.Optional(CONF_STRUCTURE): cv.string,
|
||||||
vol.Optional(CONF_SCALE, default=1): vol.Coerce(float),
|
vol.Optional(CONF_SCALE): vol.All(
|
||||||
vol.Optional(CONF_OFFSET, default=0): vol.Coerce(float),
|
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_PRECISION): cv.positive_int,
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_SWAP,
|
CONF_SWAP,
|
||||||
@@ -273,6 +281,18 @@ CLIMATE_SCHEMA = vol.All(
|
|||||||
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
|
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_COIL, "hvac_onoff_type"): cv.positive_int,
|
||||||
vol.Exclusive(CONF_HVAC_ONOFF_REGISTER, "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(
|
vol.Optional(
|
||||||
CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE
|
CONF_HVAC_ON_VALUE, default=DEFAULT_HVAC_ON_VALUE
|
||||||
): cv.positive_int,
|
): cv.positive_int,
|
||||||
@@ -385,6 +405,7 @@ CLIMATE_SCHEMA = vol.All(
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
ensure_and_check_conflicting_scales_and_offsets,
|
||||||
)
|
)
|
||||||
|
|
||||||
COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
|
COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ from .const import (
|
|||||||
CALL_TYPE_WRITE_REGISTER,
|
CALL_TYPE_WRITE_REGISTER,
|
||||||
CALL_TYPE_WRITE_REGISTERS,
|
CALL_TYPE_WRITE_REGISTERS,
|
||||||
CONF_CLIMATES,
|
CONF_CLIMATES,
|
||||||
|
CONF_CURRENT_TEMP_OFFSET,
|
||||||
|
CONF_CURRENT_TEMP_SCALE,
|
||||||
CONF_FAN_MODE_AUTO,
|
CONF_FAN_MODE_AUTO,
|
||||||
CONF_FAN_MODE_DIFFUSE,
|
CONF_FAN_MODE_DIFFUSE,
|
||||||
CONF_FAN_MODE_FOCUS,
|
CONF_FAN_MODE_FOCUS,
|
||||||
@@ -97,8 +99,12 @@ from .const import (
|
|||||||
CONF_SWING_MODE_SWING_VERT,
|
CONF_SWING_MODE_SWING_VERT,
|
||||||
CONF_SWING_MODE_VALUES,
|
CONF_SWING_MODE_VALUES,
|
||||||
CONF_TARGET_TEMP,
|
CONF_TARGET_TEMP,
|
||||||
|
CONF_TARGET_TEMP_OFFSET,
|
||||||
|
CONF_TARGET_TEMP_SCALE,
|
||||||
CONF_TARGET_TEMP_WRITE_REGISTERS,
|
CONF_TARGET_TEMP_WRITE_REGISTERS,
|
||||||
CONF_WRITE_REGISTERS,
|
CONF_WRITE_REGISTERS,
|
||||||
|
DEFAULT_OFFSET,
|
||||||
|
DEFAULT_SCALE,
|
||||||
DataType,
|
DataType,
|
||||||
)
|
)
|
||||||
from .entity import ModbusStructEntity
|
from .entity import ModbusStructEntity
|
||||||
@@ -166,6 +172,10 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
|
|||||||
self._attr_min_temp = config[CONF_MIN_TEMP]
|
self._attr_min_temp = config[CONF_MIN_TEMP]
|
||||||
self._attr_max_temp = config[CONF_MAX_TEMP]
|
self._attr_max_temp = config[CONF_MAX_TEMP]
|
||||||
self._attr_target_temperature_step = config[CONF_STEP]
|
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:
|
if CONF_HVAC_MODE_REGISTER in config:
|
||||||
mode_config = config[CONF_HVAC_MODE_REGISTER]
|
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:
|
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""Set new target temperature."""
|
||||||
target_temperature = (
|
target_temperature = (
|
||||||
float(kwargs[ATTR_TEMPERATURE]) - self._offset
|
float(kwargs[ATTR_TEMPERATURE]) - self._target_temp_offset
|
||||||
) / self._scale
|
) / self._target_temp_scale
|
||||||
if self._data_type in (
|
if self._data_type in (
|
||||||
DataType.INT16,
|
DataType.INT16,
|
||||||
DataType.INT32,
|
DataType.INT32,
|
||||||
@@ -472,15 +482,25 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
|
|||||||
self._target_temperature_register[
|
self._target_temperature_register[
|
||||||
HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode]
|
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._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
|
# Read the HVAC mode register if defined
|
||||||
if self._hvac_mode_register is not None:
|
if self._hvac_mode_register is not None:
|
||||||
hvac_mode = await self._async_read_register(
|
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
|
# Translate the value received
|
||||||
@@ -499,7 +519,11 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
|
|||||||
# Read the HVAC action register if defined
|
# Read the HVAC action register if defined
|
||||||
if self._hvac_action_register is not None:
|
if self._hvac_action_register is not None:
|
||||||
hvac_action = await self._async_read_register(
|
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
|
# Translate the value received
|
||||||
@@ -517,6 +541,8 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
|
|||||||
self._fan_mode_register
|
self._fan_mode_register
|
||||||
if isinstance(self._fan_mode_register, int)
|
if isinstance(self._fan_mode_register, int)
|
||||||
else self._fan_mode_register[0],
|
else self._fan_mode_register[0],
|
||||||
|
DEFAULT_SCALE,
|
||||||
|
DEFAULT_OFFSET,
|
||||||
raw=True,
|
raw=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -533,6 +559,8 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
|
|||||||
self._swing_mode_register
|
self._swing_mode_register
|
||||||
if isinstance(self._swing_mode_register, int)
|
if isinstance(self._swing_mode_register, int)
|
||||||
else self._swing_mode_register[0],
|
else self._swing_mode_register[0],
|
||||||
|
DEFAULT_SCALE,
|
||||||
|
DEFAULT_OFFSET,
|
||||||
raw=True,
|
raw=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -551,7 +579,11 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
|
|||||||
# in the mode register.
|
# in the mode register.
|
||||||
if self._hvac_onoff_register is not None:
|
if self._hvac_onoff_register is not None:
|
||||||
onoff = await self._async_read_register(
|
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:
|
if onoff == self._hvac_off_value:
|
||||||
self._attr_hvac_mode = HVACMode.OFF
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
@@ -562,7 +594,12 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
|
|||||||
self._attr_hvac_mode = HVACMode.OFF
|
self._attr_hvac_mode = HVACMode.OFF
|
||||||
|
|
||||||
async def _async_read_register(
|
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:
|
) -> float | None:
|
||||||
"""Read register using the Modbus hub slave."""
|
"""Read register using the Modbus hub slave."""
|
||||||
result = await self._hub.async_pb_call(
|
result = await self._hub.async_pb_call(
|
||||||
@@ -579,7 +616,7 @@ class ModbusThermostat(ModbusStructEntity, RestoreEntity, ClimateEntity):
|
|||||||
return int(result.registers[0])
|
return int(result.registers[0])
|
||||||
|
|
||||||
# The regular handling of the value
|
# 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:
|
if not self._value:
|
||||||
self._attr_available = False
|
self._attr_available = False
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ CONF_BYTESIZE = "bytesize"
|
|||||||
CONF_CLIMATES = "climates"
|
CONF_CLIMATES = "climates"
|
||||||
CONF_BRIGHTNESS_REGISTER = "brightness_address"
|
CONF_BRIGHTNESS_REGISTER = "brightness_address"
|
||||||
CONF_COLOR_TEMP_REGISTER = "color_temp_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_DATA_TYPE = "data_type"
|
||||||
CONF_DEVICE_ADDRESS = "device_address"
|
CONF_DEVICE_ADDRESS = "device_address"
|
||||||
CONF_FANS = "fans"
|
CONF_FANS = "fans"
|
||||||
@@ -48,6 +50,8 @@ CONF_SWAP_BYTE = "byte"
|
|||||||
CONF_SWAP_WORD = "word"
|
CONF_SWAP_WORD = "word"
|
||||||
CONF_SWAP_WORD_BYTE = "word_byte"
|
CONF_SWAP_WORD_BYTE = "word_byte"
|
||||||
CONF_TARGET_TEMP = "target_temp_register"
|
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_TARGET_TEMP_WRITE_REGISTERS = "target_temp_write_registers"
|
||||||
CONF_FAN_MODE_REGISTER = "fan_mode_register"
|
CONF_FAN_MODE_REGISTER = "fan_mode_register"
|
||||||
CONF_FAN_MODE_ON = "state_fan_on"
|
CONF_FAN_MODE_ON = "state_fan_on"
|
||||||
@@ -181,4 +185,7 @@ LIGHT_MODBUS_SCALE_MIN = 0
|
|||||||
LIGHT_MODBUS_SCALE_MAX = 100
|
LIGHT_MODBUS_SCALE_MAX = 100
|
||||||
LIGHT_MODBUS_INVALID_VALUE = 0xFFFF
|
LIGHT_MODBUS_INVALID_VALUE = 0xFFFF
|
||||||
|
|
||||||
|
DEFAULT_SCALE = 1.0
|
||||||
|
DEFAULT_OFFSET = 0
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__package__)
|
_LOGGER = logging.getLogger(__package__)
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ from homeassistant.const import (
|
|||||||
CONF_DELAY,
|
CONF_DELAY,
|
||||||
CONF_DEVICE_CLASS,
|
CONF_DEVICE_CLASS,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_OFFSET,
|
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_SLAVE,
|
CONF_SLAVE,
|
||||||
CONF_STRUCTURE,
|
CONF_STRUCTURE,
|
||||||
@@ -50,7 +49,6 @@ from .const import (
|
|||||||
CONF_MIN_VALUE,
|
CONF_MIN_VALUE,
|
||||||
CONF_NAN_VALUE,
|
CONF_NAN_VALUE,
|
||||||
CONF_PRECISION,
|
CONF_PRECISION,
|
||||||
CONF_SCALE,
|
|
||||||
CONF_SLAVE_COUNT,
|
CONF_SLAVE_COUNT,
|
||||||
CONF_STATE_OFF,
|
CONF_STATE_OFF,
|
||||||
CONF_STATE_ON,
|
CONF_STATE_ON,
|
||||||
@@ -62,6 +60,8 @@ from .const import (
|
|||||||
CONF_VIRTUAL_COUNT,
|
CONF_VIRTUAL_COUNT,
|
||||||
CONF_WRITE_TYPE,
|
CONF_WRITE_TYPE,
|
||||||
CONF_ZERO_SUPPRESS,
|
CONF_ZERO_SUPPRESS,
|
||||||
|
DEFAULT_OFFSET,
|
||||||
|
DEFAULT_SCALE,
|
||||||
SIGNAL_STOP_ENTITY,
|
SIGNAL_STOP_ENTITY,
|
||||||
DataType,
|
DataType,
|
||||||
)
|
)
|
||||||
@@ -163,8 +163,6 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
|
|||||||
self._swap = config[CONF_SWAP]
|
self._swap = config[CONF_SWAP]
|
||||||
self._data_type = config[CONF_DATA_TYPE]
|
self._data_type = config[CONF_DATA_TYPE]
|
||||||
self._structure: str = config[CONF_STRUCTURE]
|
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(
|
self._slave_count = config.get(CONF_SLAVE_COUNT) or config.get(
|
||||||
CONF_VIRTUAL_COUNT, 0
|
CONF_VIRTUAL_COUNT, 0
|
||||||
)
|
)
|
||||||
@@ -181,8 +179,6 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
|
|||||||
self._precision = config.get(CONF_PRECISION, 2)
|
self._precision = config.get(CONF_PRECISION, 2)
|
||||||
else:
|
else:
|
||||||
self._precision = config.get(CONF_PRECISION, 0)
|
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]:
|
def _swap_registers(self, registers: list[int], slave_count: int) -> list[int]:
|
||||||
"""Do swap as needed."""
|
"""Do swap as needed."""
|
||||||
@@ -206,7 +202,12 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
|
|||||||
registers.reverse()
|
registers.reverse()
|
||||||
return registers
|
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."""
|
"""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):
|
if self._nan_value is not None and entry in (self._nan_value, -self._nan_value):
|
||||||
return None
|
return None
|
||||||
@@ -215,7 +216,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
|
|||||||
if entry != entry: # noqa: PLR0124
|
if entry != entry: # noqa: PLR0124
|
||||||
# NaN float detection replace with None
|
# NaN float detection replace with None
|
||||||
return 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:
|
if self._min_value is not None and val < self._min_value:
|
||||||
val = self._min_value
|
val = self._min_value
|
||||||
if self._max_value is not None and val > self._max_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 str(round(val))
|
||||||
return f"{float(val):.{self._precision}f}"
|
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."""
|
"""Convert registers to proper result."""
|
||||||
|
|
||||||
if self._swap:
|
if self._swap:
|
||||||
@@ -250,7 +256,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
|
|||||||
# Apply scale, precision, limits to floats and ints
|
# Apply scale, precision, limits to floats and ints
|
||||||
v_result = []
|
v_result = []
|
||||||
for entry in val:
|
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:
|
if self._data_type != DataType.CUSTOM:
|
||||||
v_result.append(str(v_temp))
|
v_result.append(str(v_temp))
|
||||||
else:
|
else:
|
||||||
@@ -258,7 +264,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
|
|||||||
return ",".join(map(str, v_result))
|
return ",".join(map(str, v_result))
|
||||||
|
|
||||||
# Apply scale, precision, limits to floats and ints
|
# 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):
|
class ModbusToggleEntity(ModbusBaseEntity, ToggleEntity, RestoreEntity):
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
|||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_DEVICE_CLASS,
|
CONF_DEVICE_CLASS,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
|
CONF_OFFSET,
|
||||||
CONF_SENSORS,
|
CONF_SENSORS,
|
||||||
CONF_UNIQUE_ID,
|
CONF_UNIQUE_ID,
|
||||||
CONF_UNIT_OF_MEASUREMENT,
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
@@ -25,7 +26,14 @@ from homeassistant.helpers.update_coordinator import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from . import get_hub
|
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 .entity import ModbusStructEntity
|
||||||
from .modbus import ModbusHub
|
from .modbus import ModbusHub
|
||||||
|
|
||||||
@@ -73,9 +81,13 @@ class ModbusRegisterSensor(ModbusStructEntity, RestoreSensor, SensorEntity):
|
|||||||
self._coordinator: DataUpdateCoordinator[list[float | None] | None] | None = (
|
self._coordinator: DataUpdateCoordinator[list[float | None] | 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_native_unit_of_measurement = entry.get(CONF_UNIT_OF_MEASUREMENT)
|
||||||
self._attr_state_class = entry.get(CONF_STATE_CLASS)
|
self._attr_state_class = entry.get(CONF_STATE_CLASS)
|
||||||
self._attr_device_class = entry.get(CONF_DEVICE_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(
|
async def async_setup_slaves(
|
||||||
self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any]
|
self, hass: HomeAssistant, slave_count: int, entry: dict[str, Any]
|
||||||
@@ -117,7 +129,9 @@ class ModbusRegisterSensor(ModbusStructEntity, RestoreSensor, SensorEntity):
|
|||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
return
|
return
|
||||||
self._attr_available = True
|
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:
|
if self._coordinator:
|
||||||
result_array: list[float | None] = []
|
result_array: list[float | None] = []
|
||||||
if result:
|
if result:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from homeassistant.const import (
|
|||||||
CONF_COUNT,
|
CONF_COUNT,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
|
CONF_OFFSET,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_STRUCTURE,
|
CONF_STRUCTURE,
|
||||||
@@ -25,16 +26,23 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
CONF_CURRENT_TEMP_OFFSET,
|
||||||
|
CONF_CURRENT_TEMP_SCALE,
|
||||||
CONF_DATA_TYPE,
|
CONF_DATA_TYPE,
|
||||||
CONF_FAN_MODE_VALUES,
|
CONF_FAN_MODE_VALUES,
|
||||||
|
CONF_SCALE,
|
||||||
CONF_SLAVE_COUNT,
|
CONF_SLAVE_COUNT,
|
||||||
CONF_SWAP,
|
CONF_SWAP,
|
||||||
CONF_SWAP_BYTE,
|
CONF_SWAP_BYTE,
|
||||||
CONF_SWAP_WORD,
|
CONF_SWAP_WORD,
|
||||||
CONF_SWAP_WORD_BYTE,
|
CONF_SWAP_WORD_BYTE,
|
||||||
CONF_SWING_MODE_VALUES,
|
CONF_SWING_MODE_VALUES,
|
||||||
|
CONF_TARGET_TEMP_OFFSET,
|
||||||
|
CONF_TARGET_TEMP_SCALE,
|
||||||
CONF_VIRTUAL_COUNT,
|
CONF_VIRTUAL_COUNT,
|
||||||
DEFAULT_HUB,
|
DEFAULT_HUB,
|
||||||
|
DEFAULT_OFFSET,
|
||||||
|
DEFAULT_SCALE,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
@@ -243,6 +251,46 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict:
|
|||||||
return config
|
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:
|
def duplicate_swing_mode_validator(config: dict[str, Any]) -> dict:
|
||||||
"""Control modbus climate swing mode values for duplicates."""
|
"""Control modbus climate swing mode values for duplicates."""
|
||||||
swing_modes: set[int] = set()
|
swing_modes: set[int] = set()
|
||||||
|
|||||||
@@ -172,6 +172,43 @@ async def mock_modbus_fixture(
|
|||||||
return mock_pymodbus
|
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")
|
@pytest.fixture(name="mock_do_cycle")
|
||||||
async def mock_do_cycle_fixture(
|
async def mock_do_cycle_fixture(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ from homeassistant.components.climate import (
|
|||||||
from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY
|
from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY
|
||||||
from homeassistant.components.modbus.const import (
|
from homeassistant.components.modbus.const import (
|
||||||
CONF_CLIMATES,
|
CONF_CLIMATES,
|
||||||
|
CONF_CURRENT_TEMP_OFFSET,
|
||||||
|
CONF_CURRENT_TEMP_SCALE,
|
||||||
CONF_DATA_TYPE,
|
CONF_DATA_TYPE,
|
||||||
CONF_DEVICE_ADDRESS,
|
CONF_DEVICE_ADDRESS,
|
||||||
CONF_FAN_MODE_AUTO,
|
CONF_FAN_MODE_AUTO,
|
||||||
@@ -74,6 +76,7 @@ from homeassistant.components.modbus.const import (
|
|||||||
CONF_HVAC_ONOFF_REGISTER,
|
CONF_HVAC_ONOFF_REGISTER,
|
||||||
CONF_MAX_TEMP,
|
CONF_MAX_TEMP,
|
||||||
CONF_MIN_TEMP,
|
CONF_MIN_TEMP,
|
||||||
|
CONF_SCALE,
|
||||||
CONF_SWING_MODE_REGISTER,
|
CONF_SWING_MODE_REGISTER,
|
||||||
CONF_SWING_MODE_SWING_BOTH,
|
CONF_SWING_MODE_SWING_BOTH,
|
||||||
CONF_SWING_MODE_SWING_HORIZ,
|
CONF_SWING_MODE_SWING_HORIZ,
|
||||||
@@ -82,6 +85,8 @@ from homeassistant.components.modbus.const import (
|
|||||||
CONF_SWING_MODE_SWING_VERT,
|
CONF_SWING_MODE_SWING_VERT,
|
||||||
CONF_SWING_MODE_VALUES,
|
CONF_SWING_MODE_VALUES,
|
||||||
CONF_TARGET_TEMP,
|
CONF_TARGET_TEMP,
|
||||||
|
CONF_TARGET_TEMP_OFFSET,
|
||||||
|
CONF_TARGET_TEMP_SCALE,
|
||||||
CONF_TARGET_TEMP_WRITE_REGISTERS,
|
CONF_TARGET_TEMP_WRITE_REGISTERS,
|
||||||
CONF_WRITE_REGISTERS,
|
CONF_WRITE_REGISTERS,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -92,6 +97,7 @@ from homeassistant.const import (
|
|||||||
ATTR_TEMPERATURE,
|
ATTR_TEMPERATURE,
|
||||||
CONF_ADDRESS,
|
CONF_ADDRESS,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
|
CONF_OFFSET,
|
||||||
CONF_PLATFORM,
|
CONF_PLATFORM,
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_SLAVE,
|
CONF_SLAVE,
|
||||||
@@ -1699,3 +1705,223 @@ async def test_no_discovery_info_climate(
|
|||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
assert CLIMATE_DOMAIN in hass.config.components
|
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_BAUDRATE,
|
||||||
CONF_BYTESIZE,
|
CONF_BYTESIZE,
|
||||||
CONF_CLIMATES,
|
CONF_CLIMATES,
|
||||||
|
CONF_CURRENT_TEMP_OFFSET,
|
||||||
|
CONF_CURRENT_TEMP_SCALE,
|
||||||
CONF_DATA_TYPE,
|
CONF_DATA_TYPE,
|
||||||
CONF_DEVICE_ADDRESS,
|
CONF_DEVICE_ADDRESS,
|
||||||
CONF_FAN_MODE_HIGH,
|
CONF_FAN_MODE_HIGH,
|
||||||
@@ -51,6 +53,7 @@ from homeassistant.components.modbus.const import (
|
|||||||
CONF_INPUT_TYPE,
|
CONF_INPUT_TYPE,
|
||||||
CONF_MSG_WAIT,
|
CONF_MSG_WAIT,
|
||||||
CONF_PARITY,
|
CONF_PARITY,
|
||||||
|
CONF_SCALE,
|
||||||
CONF_SLAVE_COUNT,
|
CONF_SLAVE_COUNT,
|
||||||
CONF_STOPBITS,
|
CONF_STOPBITS,
|
||||||
CONF_SWAP,
|
CONF_SWAP,
|
||||||
@@ -61,6 +64,9 @@ from homeassistant.components.modbus.const import (
|
|||||||
CONF_SWING_MODE_SWING_OFF,
|
CONF_SWING_MODE_SWING_OFF,
|
||||||
CONF_SWING_MODE_SWING_ON,
|
CONF_SWING_MODE_SWING_ON,
|
||||||
CONF_SWING_MODE_VALUES,
|
CONF_SWING_MODE_VALUES,
|
||||||
|
CONF_TARGET_TEMP,
|
||||||
|
CONF_TARGET_TEMP_OFFSET,
|
||||||
|
CONF_TARGET_TEMP_SCALE,
|
||||||
CONF_VIRTUAL_COUNT,
|
CONF_VIRTUAL_COUNT,
|
||||||
DEFAULT_SCAN_INTERVAL,
|
DEFAULT_SCAN_INTERVAL,
|
||||||
DEVICE_ID,
|
DEVICE_ID,
|
||||||
@@ -78,8 +84,10 @@ from homeassistant.components.modbus.validators import (
|
|||||||
check_config,
|
check_config,
|
||||||
duplicate_fan_mode_validator,
|
duplicate_fan_mode_validator,
|
||||||
duplicate_swing_mode_validator,
|
duplicate_swing_mode_validator,
|
||||||
|
ensure_and_check_conflicting_scales_and_offsets,
|
||||||
hvac_fixedsize_reglist_validator,
|
hvac_fixedsize_reglist_validator,
|
||||||
nan_validator,
|
nan_validator,
|
||||||
|
not_zero_value,
|
||||||
register_int_list_validator,
|
register_int_list_validator,
|
||||||
struct_validator,
|
struct_validator,
|
||||||
)
|
)
|
||||||
@@ -93,6 +101,7 @@ from homeassistant.const import (
|
|||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_METHOD,
|
CONF_METHOD,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
|
CONF_OFFSET,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
CONF_SCAN_INTERVAL,
|
CONF_SCAN_INTERVAL,
|
||||||
CONF_SENSORS,
|
CONF_SENSORS,
|
||||||
@@ -1556,3 +1565,70 @@ async def test_pb_service_write_no_slave(
|
|||||||
|
|
||||||
if do_return[DATA]:
|
if do_return[DATA]:
|
||||||
assert any(message.startswith("Pymodbus:") for message in caplog.messages)
|
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()
|
await hass.async_block_till_done()
|
||||||
assert SENSOR_DOMAIN in hass.config.components
|
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