Add modbus option to manage different setPoint registers (#107600)

Co-authored-by: jan iversen <jancasacondor@gmail.com>
This commit is contained in:
Claudio Ruggeri - CR-Tech 2024-02-17 19:48:42 +01:00 committed by GitHub
parent 53944235d2
commit 1ded412061
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 221 additions and 20 deletions

View file

@ -134,7 +134,9 @@ from .const import ( # noqa: F401
from .modbus import ModbusHub, async_modbus_setup
from .validators import (
check_config,
check_hvac_target_temp_registers,
duplicate_fan_mode_validator,
hvac_fixedsize_reglist_validator,
nan_validator,
register_int_list_validator,
struct_validator,
@ -239,7 +241,7 @@ BASE_SWITCH_SCHEMA = BASE_COMPONENT_SCHEMA.extend(
CLIMATE_SCHEMA = vol.All(
BASE_STRUCT_SCHEMA.extend(
{
vol.Required(CONF_TARGET_TEMP): cv.positive_int,
vol.Required(CONF_TARGET_TEMP): hvac_fixedsize_reglist_validator,
vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean,
vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(float),
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float),
@ -296,8 +298,9 @@ CLIMATE_SCHEMA = vol.All(
duplicate_fan_mode_validator,
),
),
}
},
),
check_hvac_target_temp_registers,
)
COVERS_SCHEMA = BASE_COMPONENT_SCHEMA.extend(

View file

@ -75,6 +75,17 @@ from .modbus import ModbusHub
PARALLEL_UPDATES = 1
HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY = {
HVACMode.AUTO: 0,
HVACMode.COOL: 1,
HVACMode.DRY: 2,
HVACMode.FAN_ONLY: 3,
HVACMode.HEAT: 4,
HVACMode.HEAT_COOL: 5,
HVACMode.OFF: 6,
None: 0,
}
async def async_setup_platform(
hass: HomeAssistant,
@ -117,7 +128,6 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
CONF_TARGET_TEMP_WRITE_REGISTERS
]
self._unit = config[CONF_TEMPERATURE_UNIT]
self._attr_current_temperature = None
self._attr_target_temperature = None
self._attr_temperature_unit = (
@ -157,7 +167,6 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
for value in values:
self._hvac_mode_mapping.append((value, hvac_mode))
self._attr_hvac_modes.append(hvac_mode)
else:
# No HVAC modes defined
self._hvac_mode_register = None
@ -305,21 +314,27 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
if self._target_temperature_write_registers:
result = await self._hub.async_pb_call(
self._slave,
self._target_temperature_register,
self._target_temperature_register[
HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode]
],
[int(float(registers[0]))],
CALL_TYPE_WRITE_REGISTERS,
)
else:
result = await self._hub.async_pb_call(
self._slave,
self._target_temperature_register,
self._target_temperature_register[
HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode]
],
int(float(registers[0])),
CALL_TYPE_WRITE_REGISTER,
)
else:
result = await self._hub.async_pb_call(
self._slave,
self._target_temperature_register,
self._target_temperature_register[
HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode]
],
[int(float(i)) for i in registers],
CALL_TYPE_WRITE_REGISTERS,
)
@ -332,12 +347,15 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
# async_track_time_interval
self._attr_target_temperature = await self._async_read_register(
CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register
CALL_TYPE_REGISTER_HOLDING,
self._target_temperature_register[
HVACMODE_TO_TARG_TEMP_REG_INDEX_ARRAY[self._attr_hvac_mode]
],
)
self._attr_current_temperature = await self._async_read_register(
self._input_type, self._address
)
# Read the HVAC mode register if defined
if self._hvac_mode_register is not None:
hvac_mode = await self._async_read_register(

View file

@ -8,6 +8,7 @@ from typing import Any
import voluptuous as vol
from homeassistant.components.climate import HVACMode
from homeassistant.const import (
CONF_ADDRESS,
CONF_COMMAND_OFF,
@ -29,6 +30,7 @@ from .const import (
CONF_FAN_MODE_REGISTER,
CONF_FAN_MODE_VALUES,
CONF_HVAC_MODE_REGISTER,
CONF_HVAC_ONOFF_REGISTER,
CONF_INPUT_TYPE,
CONF_SLAVE_COUNT,
CONF_SWAP,
@ -172,6 +174,26 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]:
}
def hvac_fixedsize_reglist_validator(value: Any) -> list:
"""Check the number of registers for target temp. and coerce it to a list, if valid."""
if isinstance(value, int):
value = [value] * len(HVACMode)
return list(value)
if len(value) == len(HVACMode):
_rv = True
for svalue in value:
if isinstance(svalue, int) is False:
_rv = False
break
if _rv is True:
return list(value)
raise vol.Invalid(
f"Invalid target temp register. Required type: integer, allowed 1 or list of {len(HVACMode)} registers"
)
def nan_validator(value: Any) -> int:
"""Convert nan string to number (can be hex string or int)."""
if isinstance(value, int):
@ -203,6 +225,34 @@ def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict:
return config
def check_hvac_target_temp_registers(config: dict) -> dict:
"""Check conflicts among HVAC target temperature registers and HVAC ON/OFF, HVAC register, Fan Modes."""
if (
CONF_HVAC_MODE_REGISTER in config
and config[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS] in config[CONF_TARGET_TEMP]
):
wrn = f"{CONF_HVAC_MODE_REGISTER} overlaps CONF_TARGET_TEMP register(s). {CONF_HVAC_MODE_REGISTER} is not loaded!"
_LOGGER.warning(wrn)
del config[CONF_HVAC_MODE_REGISTER]
if (
CONF_HVAC_ONOFF_REGISTER in config
and config[CONF_HVAC_ONOFF_REGISTER] in config[CONF_TARGET_TEMP]
):
wrn = f"{CONF_HVAC_ONOFF_REGISTER} overlaps CONF_TARGET_TEMP register(s). {CONF_HVAC_ONOFF_REGISTER} is not loaded!"
_LOGGER.warning(wrn)
del config[CONF_HVAC_ONOFF_REGISTER]
if (
CONF_FAN_MODE_REGISTER in config
and config[CONF_FAN_MODE_REGISTER][CONF_ADDRESS] in config[CONF_TARGET_TEMP]
):
wrn = f"{CONF_FAN_MODE_REGISTER} overlaps CONF_TARGET_TEMP register(s). {CONF_FAN_MODE_REGISTER} is not loaded!"
_LOGGER.warning(wrn)
del config[CONF_FAN_MODE_REGISTER]
return config
def register_int_list_validator(value: Any) -> Any:
"""Check if a register (CONF_ADRESS) is an int or a list having only 1 register."""
if isinstance(value, int) and value >= 0:

View file

@ -129,7 +129,7 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_")
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_TARGET_TEMP: [130, 131, 132, 133, 135, 128, 129],
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_HVAC_ONOFF_REGISTER: 12,
@ -374,7 +374,7 @@ async def test_temperature_error(hass: HomeAssistant, expected, mock_do_cycle) -
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_TARGET_TEMP: [130, 131, 132, 133, 134, 135, 136],
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
@ -398,7 +398,7 @@ async def test_temperature_error(hass: HomeAssistant, expected, mock_do_cycle) -
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_TARGET_TEMP: 119,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
@ -422,7 +422,7 @@ async def test_temperature_error(hass: HomeAssistant, expected, mock_do_cycle) -
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_TARGET_TEMP: 120,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
@ -464,7 +464,7 @@ async def test_service_climate_update(
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_TARGET_TEMP: 116,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
@ -488,7 +488,7 @@ async def test_service_climate_update(
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_TARGET_TEMP: 116,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
@ -512,7 +512,7 @@ async def test_service_climate_update(
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_TARGET_TEMP: 116,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
@ -641,7 +641,7 @@ async def test_service_climate_fan_update(
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_TARGET_TEMP: [150, 151, 152, 153, 154, 155, 156],
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_DATA_TYPE: DataType.INT16,
@ -767,6 +767,7 @@ async def test_service_set_hvac_mode(
) -> None:
"""Test set HVAC mode."""
mock_modbus.read_holding_registers.return_value = ReadResult(result)
await hass.services.async_call(
CLIMATE_DOMAIN,
"set_hvac_mode",

View file

@ -50,9 +50,12 @@ from homeassistant.components.modbus.const import (
CONF_FAN_MODE_REGISTER,
CONF_FAN_MODE_VALUES,
CONF_HVAC_MODE_COOL,
CONF_HVAC_MODE_DRY,
CONF_HVAC_MODE_HEAT,
CONF_HVAC_MODE_HEAT_COOL,
CONF_HVAC_MODE_REGISTER,
CONF_HVAC_MODE_VALUES,
CONF_HVAC_ONOFF_REGISTER,
CONF_INPUT_TYPE,
CONF_MSG_WAIT,
CONF_PARITY,
@ -80,7 +83,9 @@ from homeassistant.components.modbus.const import (
)
from homeassistant.components.modbus.validators import (
check_config,
check_hvac_target_temp_registers,
duplicate_fan_mode_validator,
hvac_fixedsize_reglist_validator,
nan_validator,
register_int_list_validator,
struct_validator,
@ -137,6 +142,22 @@ async def mock_modbus_with_pymodbus_fixture(hass, caplog, do_config, mock_pymodb
return mock_pymodbus
async def test_fixedRegList_validator() -> None:
"""Test fixed temp registers validator."""
for value in (
15,
[30, 31, 32, 33, 34, 35, 36],
):
assert isinstance(hvac_fixedsize_reglist_validator(value), list)
with pytest.raises(vol.Invalid):
hvac_fixedsize_reglist_validator([15, "ab", 17, 18, 19, 20, 21])
with pytest.raises(vol.Invalid):
hvac_fixedsize_reglist_validator([15, 17])
async def test_register_int_list_validator() -> None:
"""Test conf address register validator."""
for value, vtype in (
@ -536,7 +557,7 @@ async def test_check_config_sensor(do_config) -> None:
CONF_NAME: TEST_ENTITY_NAME + " 2",
CONF_ADDRESS: 118,
CONF_SLAVE: 0,
CONF_TARGET_TEMP: 99,
CONF_TARGET_TEMP: [99],
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 120,
CONF_FAN_MODE_VALUES: {
@ -570,9 +591,9 @@ async def test_check_config_sensor(do_config) -> None:
},
{
CONF_NAME: TEST_ENTITY_NAME + " 2",
CONF_ADDRESS: 118,
CONF_ADDRESS: 117,
CONF_SLAVE: 0,
CONF_TARGET_TEMP: 117,
CONF_TARGET_TEMP: [117],
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: [121],
CONF_FAN_MODE_VALUES: {
@ -584,6 +605,61 @@ async def test_check_config_sensor(do_config) -> None:
],
}
],
[
{
CONF_NAME: TEST_MODBUS_NAME,
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
CONF_TIMEOUT: 3,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 117,
CONF_TARGET_TEMP: [130, 131, 132, 133, 134, 135, 136],
CONF_SLAVE: 0,
CONF_HVAC_MODE_REGISTER: {
CONF_ADDRESS: 118,
CONF_HVAC_MODE_VALUES: {
CONF_HVAC_MODE_COOL: 0,
CONF_HVAC_MODE_HEAT: 2,
CONF_HVAC_MODE_DRY: 3,
},
},
CONF_HVAC_ONOFF_REGISTER: 122,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 120,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_ON: 0,
CONF_FAN_MODE_HIGH: 1,
},
},
},
{
CONF_NAME: TEST_ENTITY_NAME + " 2",
CONF_ADDRESS: 118,
CONF_TARGET_TEMP: [130, 131, 132, 133, 134, 135, 136],
CONF_SLAVE: 0,
CONF_HVAC_MODE_REGISTER: {
CONF_ADDRESS: 130,
CONF_HVAC_MODE_VALUES: {
CONF_HVAC_MODE_COOL: 0,
CONF_HVAC_MODE_HEAT: 2,
CONF_HVAC_MODE_DRY: 3,
},
},
CONF_HVAC_ONOFF_REGISTER: 122,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 120,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_ON: 0,
CONF_FAN_MODE_HIGH: 1,
},
},
},
],
}
],
],
)
async def test_check_config_climate(do_config) -> None:
@ -592,6 +668,59 @@ async def test_check_config_climate(do_config) -> None:
assert len(do_config[0][CONF_CLIMATES]) == 1
@pytest.mark.parametrize(
"do_config",
[
[
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 1,
CONF_TARGET_TEMP: [117, 121, 119, 150, 151, 152, 156],
CONF_HVAC_MODE_REGISTER: {
CONF_ADDRESS: 119,
CONF_HVAC_MODE_VALUES: {
CONF_HVAC_MODE_COOL: 0,
CONF_HVAC_MODE_HEAT: 1,
CONF_HVAC_MODE_HEAT_COOL: 2,
CONF_HVAC_MODE_DRY: 3,
},
},
CONF_HVAC_ONOFF_REGISTER: 117,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 121,
},
},
],
[
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 1,
CONF_TARGET_TEMP: [117],
CONF_HVAC_MODE_REGISTER: {
CONF_ADDRESS: 117,
CONF_HVAC_MODE_VALUES: {
CONF_HVAC_MODE_COOL: 0,
CONF_HVAC_MODE_HEAT: 1,
CONF_HVAC_MODE_HEAT_COOL: 2,
CONF_HVAC_MODE_DRY: 3,
},
},
CONF_HVAC_ONOFF_REGISTER: 117,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 117,
},
},
],
],
)
async def test_climate_conflict_addresses(do_config) -> None:
"""Test conflicts among the addresses of target temp and other climate addresses."""
check_hvac_target_temp_registers(do_config[0])
assert CONF_HVAC_MODE_REGISTER not in do_config[0]
assert CONF_HVAC_ONOFF_REGISTER not in do_config[0]
assert CONF_FAN_MODE_REGISTER not in do_config[0]
@pytest.mark.parametrize(
"do_config",
[