Add modbus option to manage different setPoint registers (#107600)
Co-authored-by: jan iversen <jancasacondor@gmail.com>
This commit is contained in:
parent
53944235d2
commit
1ded412061
5 changed files with 221 additions and 20 deletions
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
[
|
||||
|
|
Loading…
Add table
Reference in a new issue