Add Modbus fan speed support (#104577)

Co-authored-by: jan iversen <jancasacondor@gmail.com>
This commit is contained in:
Cían Hughes 2023-12-07 07:19:03 +00:00 committed by GitHub
parent 22119a2fd8
commit e051244927
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 587 additions and 16 deletions

View file

@ -63,6 +63,18 @@ from .const import ( # noqa: F401
CONF_CLOSE_COMM_ON_ERROR,
CONF_DATA_TYPE,
CONF_DEVICE_ADDRESS,
CONF_FAN_MODE_AUTO,
CONF_FAN_MODE_DIFFUSE,
CONF_FAN_MODE_FOCUS,
CONF_FAN_MODE_HIGH,
CONF_FAN_MODE_LOW,
CONF_FAN_MODE_MEDIUM,
CONF_FAN_MODE_MIDDLE,
CONF_FAN_MODE_OFF,
CONF_FAN_MODE_ON,
CONF_FAN_MODE_REGISTER,
CONF_FAN_MODE_TOP,
CONF_FAN_MODE_VALUES,
CONF_FANS,
CONF_HVAC_MODE_AUTO,
CONF_HVAC_MODE_COOL,
@ -122,6 +134,7 @@ from .const import ( # noqa: F401
from .modbus import ModbusHub, async_modbus_setup
from .validators import (
duplicate_entity_validator,
duplicate_fan_mode_validator,
duplicate_modbus_validator,
nan_validator,
number_validator,
@ -265,6 +278,26 @@ CLIMATE_SCHEMA = vol.All(
vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean,
}
),
vol.Optional(CONF_FAN_MODE_REGISTER): vol.Maybe(
vol.All(
{
CONF_ADDRESS: cv.positive_int,
CONF_FAN_MODE_VALUES: {
vol.Optional(CONF_FAN_MODE_ON): cv.positive_int,
vol.Optional(CONF_FAN_MODE_OFF): cv.positive_int,
vol.Optional(CONF_FAN_MODE_AUTO): cv.positive_int,
vol.Optional(CONF_FAN_MODE_LOW): cv.positive_int,
vol.Optional(CONF_FAN_MODE_MEDIUM): cv.positive_int,
vol.Optional(CONF_FAN_MODE_HIGH): cv.positive_int,
vol.Optional(CONF_FAN_MODE_TOP): cv.positive_int,
vol.Optional(CONF_FAN_MODE_MIDDLE): cv.positive_int,
vol.Optional(CONF_FAN_MODE_FOCUS): cv.positive_int,
vol.Optional(CONF_FAN_MODE_DIFFUSE): cv.positive_int,
},
},
duplicate_fan_mode_validator,
),
),
}
),
)

View file

@ -6,6 +6,16 @@ import struct
from typing import Any, cast
from homeassistant.components.climate import (
FAN_AUTO,
FAN_DIFFUSE,
FAN_FOCUS,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_MIDDLE,
FAN_OFF,
FAN_ON,
FAN_TOP,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
@ -31,6 +41,18 @@ from .const import (
CALL_TYPE_WRITE_REGISTER,
CALL_TYPE_WRITE_REGISTERS,
CONF_CLIMATES,
CONF_FAN_MODE_AUTO,
CONF_FAN_MODE_DIFFUSE,
CONF_FAN_MODE_FOCUS,
CONF_FAN_MODE_HIGH,
CONF_FAN_MODE_LOW,
CONF_FAN_MODE_MEDIUM,
CONF_FAN_MODE_MIDDLE,
CONF_FAN_MODE_OFF,
CONF_FAN_MODE_ON,
CONF_FAN_MODE_REGISTER,
CONF_FAN_MODE_TOP,
CONF_FAN_MODE_VALUES,
CONF_HVAC_MODE_AUTO,
CONF_HVAC_MODE_COOL,
CONF_HVAC_MODE_DRY,
@ -138,6 +160,42 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
self._attr_hvac_mode = HVACMode.AUTO
self._attr_hvac_modes = [HVACMode.AUTO]
if CONF_FAN_MODE_REGISTER in config:
self._attr_supported_features = (
self._attr_supported_features | ClimateEntityFeature.FAN_MODE
)
mode_config = config[CONF_FAN_MODE_REGISTER]
self._fan_mode_register = mode_config[CONF_ADDRESS]
self._attr_fan_modes = cast(list[str], [])
self._attr_fan_mode = None
self._fan_mode_mapping_to_modbus: dict[str, int] = {}
self._fan_mode_mapping_from_modbus: dict[int, str] = {}
mode_value_config = mode_config[CONF_FAN_MODE_VALUES]
for fan_mode_kw, fan_mode in (
(CONF_FAN_MODE_ON, FAN_ON),
(CONF_FAN_MODE_OFF, FAN_OFF),
(CONF_FAN_MODE_AUTO, FAN_AUTO),
(CONF_FAN_MODE_LOW, FAN_LOW),
(CONF_FAN_MODE_MEDIUM, FAN_MEDIUM),
(CONF_FAN_MODE_HIGH, FAN_HIGH),
(CONF_FAN_MODE_TOP, FAN_TOP),
(CONF_FAN_MODE_MIDDLE, FAN_MIDDLE),
(CONF_FAN_MODE_FOCUS, FAN_FOCUS),
(CONF_FAN_MODE_DIFFUSE, FAN_DIFFUSE),
):
if fan_mode_kw in mode_value_config:
value = mode_value_config[fan_mode_kw]
self._fan_mode_mapping_from_modbus[value] = fan_mode
self._fan_mode_mapping_to_modbus[fan_mode] = value
self._attr_fan_modes.append(fan_mode)
else:
# No HVAC modes defined
self._fan_mode_register = None
self._attr_fan_mode = FAN_AUTO
self._attr_fan_modes = [FAN_AUTO]
if CONF_HVAC_ONOFF_REGISTER in config:
self._hvac_onoff_register = config[CONF_HVAC_ONOFF_REGISTER]
self._hvac_onoff_write_registers = config[CONF_WRITE_REGISTERS]
@ -194,6 +252,21 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
await self.async_update()
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
if self._fan_mode_register is not None:
# Write a value to the mode register for the desired mode.
value = self._fan_mode_mapping_to_modbus[fan_mode]
await self._hub.async_pb_call(
self._slave,
self._fan_mode_register,
value,
CALL_TYPE_WRITE_REGISTER,
)
await self.async_update()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
target_temperature = (
@ -255,7 +328,7 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
self._input_type, self._address
)
# Read the mode register if defined
# 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
@ -269,7 +342,17 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
self._attr_hvac_mode = mode
break
# Read th on/off register if defined. If the value in this
# Read the Fan mode register if defined
if self._fan_mode_register is not None:
fan_mode = await self._async_read_register(
CALL_TYPE_REGISTER_HOLDING, self._fan_mode_register, raw=True
)
# Translate the value received
if fan_mode is not None:
self._attr_fan_mode = self._fan_mode_mapping_from_modbus[int(fan_mode)]
# Read the on/off register if defined. If the value in this
# register is "OFF", it will take precedence over the value
# in the mode register.
if self._hvac_onoff_register is not None:

View file

@ -49,8 +49,19 @@ CONF_SWAP_WORD = "word"
CONF_SWAP_WORD_BYTE = "word_byte"
CONF_TARGET_TEMP = "target_temp_register"
CONF_TARGET_TEMP_WRITE_REGISTERS = "target_temp_write_registers"
CONF_FAN_MODE_REGISTER = "fan_mode_register"
CONF_FAN_MODE_ON = "state_fan_on"
CONF_FAN_MODE_OFF = "state_fan_off"
CONF_FAN_MODE_LOW = "state_fan_low"
CONF_FAN_MODE_MEDIUM = "state_fan_medium"
CONF_FAN_MODE_HIGH = "state_fan_high"
CONF_FAN_MODE_AUTO = "state_fan_auto"
CONF_FAN_MODE_TOP = "state_fan_top"
CONF_FAN_MODE_MIDDLE = "state_fan_middle"
CONF_FAN_MODE_FOCUS = "state_fan_focus"
CONF_FAN_MODE_DIFFUSE = "state_fan_diffuse"
CONF_FAN_MODE_VALUES = "values"
CONF_HVAC_MODE_REGISTER = "hvac_mode_register"
CONF_HVAC_MODE_VALUES = "values"
CONF_HVAC_ONOFF_REGISTER = "hvac_onoff_register"
CONF_HVAC_MODE_OFF = "state_off"
CONF_HVAC_MODE_HEAT = "state_heat"
@ -59,6 +70,7 @@ CONF_HVAC_MODE_HEAT_COOL = "state_heat_cool"
CONF_HVAC_MODE_AUTO = "state_auto"
CONF_HVAC_MODE_DRY = "state_dry"
CONF_HVAC_MODE_FAN_ONLY = "state_fan_only"
CONF_HVAC_MODE_VALUES = "values"
CONF_WRITE_REGISTERS = "write_registers"
CONF_VERIFY = "verify"
CONF_VIRTUAL_COUNT = "virtual_count"

View file

@ -26,12 +26,16 @@ from homeassistant.const import (
from .const import (
CONF_DATA_TYPE,
CONF_DEVICE_ADDRESS,
CONF_FAN_MODE_REGISTER,
CONF_FAN_MODE_VALUES,
CONF_HVAC_MODE_REGISTER,
CONF_INPUT_TYPE,
CONF_SLAVE_COUNT,
CONF_SWAP,
CONF_SWAP_BYTE,
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
CONF_TARGET_TEMP,
CONF_VIRTUAL_COUNT,
CONF_WRITE_TYPE,
DEFAULT_HUB,
@ -264,12 +268,31 @@ def duplicate_entity_validator(config: dict) -> dict:
addr += "_" + str(entry[CONF_COMMAND_OFF])
inx = entry.get(CONF_SLAVE, None) or entry.get(CONF_DEVICE_ADDRESS, 0)
addr += "_" + str(inx)
if addr in addresses:
err = (
f"Modbus {component}/{name} address {addr} is duplicate, second"
" entry not loaded!"
)
_LOGGER.warning(err)
entry_addrs: set[str] = set()
entry_addrs.add(addr)
if CONF_TARGET_TEMP in entry:
a = str(entry[CONF_TARGET_TEMP])
a += "_" + str(inx)
entry_addrs.add(a)
if CONF_HVAC_MODE_REGISTER in entry:
a = str(entry[CONF_HVAC_MODE_REGISTER][CONF_ADDRESS])
a += "_" + str(inx)
entry_addrs.add(a)
if CONF_FAN_MODE_REGISTER in entry:
a = str(entry[CONF_FAN_MODE_REGISTER][CONF_ADDRESS])
a += "_" + str(inx)
entry_addrs.add(a)
dup_addrs = entry_addrs.intersection(addresses)
if len(dup_addrs) > 0:
for addr in dup_addrs:
err = (
f"Modbus {component}/{name} address {addr} is duplicate, second"
" entry not loaded!"
)
_LOGGER.warning(err)
errors.append(index)
elif name in names:
err = (
@ -280,7 +303,7 @@ def duplicate_entity_validator(config: dict) -> dict:
errors.append(index)
else:
names.add(name)
addresses.add(addr)
addresses.update(entry_addrs)
for i in reversed(errors):
del config[hub_index][conf_key][i]
@ -299,11 +322,11 @@ def duplicate_modbus_validator(config: list) -> list:
else:
host = f"{hub[CONF_HOST]}_{hub[CONF_PORT]}"
if host in hosts:
err = f"Modbus {name}  contains duplicate host/port {host}, not loaded!"
err = f"Modbus {name} contains duplicate host/port {host}, not loaded!"
_LOGGER.warning(err)
errors.append(index)
elif name in names:
err = f"Modbus {name}  is duplicate, second entry not loaded!"
err = f"Modbus {name} is duplicate, second entry not loaded!"
_LOGGER.warning(err)
errors.append(index)
else:
@ -313,3 +336,20 @@ def duplicate_modbus_validator(config: list) -> list:
for i in reversed(errors):
del config[i]
return config
def duplicate_fan_mode_validator(config: dict[str, Any]) -> dict:
"""Control modbus climate fan mode values for duplicates."""
fan_modes: set[int] = set()
errors = []
for key, value in config[CONF_FAN_MODE_VALUES].items():
if value in fan_modes:
wrn = f"Modbus fan mode {key} has a duplicate value {value}, not loaded, values must be unique!"
_LOGGER.warning(wrn)
errors.append(key)
else:
fan_modes.add(value)
for key in reversed(errors):
del config[CONF_FAN_MODE_VALUES][key]
return config

View file

@ -3,14 +3,34 @@ import pytest
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.climate.const import (
ATTR_FAN_MODE,
ATTR_FAN_MODES,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
FAN_AUTO,
FAN_DIFFUSE,
FAN_FOCUS,
FAN_HIGH,
FAN_LOW,
FAN_MEDIUM,
FAN_MIDDLE,
FAN_OFF,
FAN_ON,
FAN_TOP,
HVACMode,
)
from homeassistant.components.modbus.const import (
CONF_CLIMATES,
CONF_DATA_TYPE,
CONF_DEVICE_ADDRESS,
CONF_FAN_MODE_AUTO,
CONF_FAN_MODE_HIGH,
CONF_FAN_MODE_LOW,
CONF_FAN_MODE_MEDIUM,
CONF_FAN_MODE_OFF,
CONF_FAN_MODE_ON,
CONF_FAN_MODE_REGISTER,
CONF_FAN_MODE_VALUES,
CONF_HVAC_MODE_AUTO,
CONF_HVAC_MODE_COOL,
CONF_HVAC_MODE_DRY,
@ -183,7 +203,7 @@ async def test_config_climate(hass: HomeAssistant, mock_modbus) -> None:
],
)
async def test_config_hvac_mode_register(hass: HomeAssistant, mock_modbus) -> None:
"""Run configuration test for mode register."""
"""Run configuration test for HVAC mode register."""
state = hass.states.get(ENTITY_ID)
assert HVACMode.OFF in state.attributes[ATTR_HVAC_MODES]
assert HVACMode.HEAT in state.attributes[ATTR_HVAC_MODES]
@ -193,6 +213,47 @@ async def test_config_hvac_mode_register(hass: HomeAssistant, mock_modbus) -> No
assert HVACMode.FAN_ONLY in state.attributes[ATTR_HVAC_MODES]
@pytest.mark.parametrize(
"do_config",
[
{
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 11,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_ON: 0,
CONF_FAN_MODE_OFF: 1,
CONF_FAN_MODE_AUTO: 2,
CONF_FAN_MODE_LOW: 3,
CONF_FAN_MODE_MEDIUM: 4,
CONF_FAN_MODE_HIGH: 5,
},
},
}
],
},
],
)
async def test_config_fan_mode_register(hass: HomeAssistant, mock_modbus) -> None:
"""Run configuration test for Fan mode register."""
state = hass.states.get(ENTITY_ID)
assert FAN_ON in state.attributes[ATTR_FAN_MODES]
assert FAN_OFF in state.attributes[ATTR_FAN_MODES]
assert FAN_AUTO in state.attributes[ATTR_FAN_MODES]
assert FAN_LOW in state.attributes[ATTR_FAN_MODES]
assert FAN_MEDIUM in state.attributes[ATTR_FAN_MODES]
assert FAN_HIGH in state.attributes[ATTR_FAN_MODES]
assert FAN_TOP not in state.attributes[ATTR_FAN_MODES]
assert FAN_MIDDLE not in state.attributes[ATTR_FAN_MODES]
assert FAN_DIFFUSE not in state.attributes[ATTR_FAN_MODES]
assert FAN_FOCUS not in state.attributes[ATTR_FAN_MODES]
@pytest.mark.parametrize(
"do_config",
[
@ -338,6 +399,96 @@ async def test_service_climate_update(
assert hass.states.get(ENTITY_ID).state == result
@pytest.mark.parametrize(
("do_config", "result", "register_words"),
[
(
{
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
CONF_DATA_TYPE: DataType.INT32,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 118,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_LOW: 0,
CONF_FAN_MODE_MEDIUM: 1,
CONF_FAN_MODE_HIGH: 2,
},
},
},
]
},
FAN_LOW,
[0x00],
),
(
{
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
CONF_DATA_TYPE: DataType.INT32,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 118,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_LOW: 0,
CONF_FAN_MODE_MEDIUM: 1,
CONF_FAN_MODE_HIGH: 2,
},
},
},
]
},
FAN_MEDIUM,
[0x01],
),
(
{
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_SCAN_INTERVAL: 0,
CONF_DATA_TYPE: DataType.INT32,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 118,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_LOW: 0,
CONF_FAN_MODE_MEDIUM: 1,
CONF_FAN_MODE_HIGH: 2,
},
},
CONF_HVAC_ONOFF_REGISTER: 119,
},
]
},
FAN_HIGH,
[0x02],
),
],
)
async def test_service_climate_fan_update(
hass: HomeAssistant, mock_modbus, mock_ha, result, register_words
) -> None:
"""Run test for service homeassistant.update_entity."""
mock_modbus.read_holding_registers.return_value = ReadResult(register_words)
await hass.services.async_call(
"homeassistant", "update_entity", {"entity_id": ENTITY_ID}, blocking=True
)
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).attributes[ATTR_FAN_MODE] == result
@pytest.mark.parametrize(
("temperature", "result", "do_config"),
[
@ -529,10 +680,10 @@ async def test_service_climate_set_temperature(
),
],
)
async def test_service_set_mode(
async def test_service_set_hvac_mode(
hass: HomeAssistant, hvac_mode, result, mock_modbus, mock_ha
) -> None:
"""Test set mode."""
"""Test set HVAC mode."""
mock_modbus.read_holding_registers.return_value = ReadResult(result)
await hass.services.async_call(
CLIMATE_DOMAIN,
@ -545,6 +696,69 @@ async def test_service_set_mode(
)
@pytest.mark.parametrize(
("fan_mode", "result", "do_config"),
[
(
FAN_OFF,
[0x02],
{
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 118,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_ON: 1,
CONF_FAN_MODE_OFF: 2,
},
},
}
]
},
),
(
FAN_ON,
[0x01],
{
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_TARGET_TEMP: 117,
CONF_ADDRESS: 117,
CONF_SLAVE: 10,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 118,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_ON: 1,
CONF_FAN_MODE_OFF: 2,
},
},
}
]
},
),
],
)
async def test_service_set_fan_mode(
hass: HomeAssistant, fan_mode, result, mock_modbus, mock_ha
) -> None:
"""Test set Fan mode."""
mock_modbus.read_holding_registers.return_value = ReadResult(result)
await hass.services.async_call(
CLIMATE_DOMAIN,
"set_fan_mode",
{
"entity_id": ENTITY_ID,
ATTR_FAN_MODE: fan_mode,
},
blocking=True,
)
test_value = State(ENTITY_ID, 35)
test_value.attributes = {ATTR_TEMPERATURE: 37}

View file

@ -40,9 +40,19 @@ from homeassistant.components.modbus.const import (
CALL_TYPE_WRITE_REGISTERS,
CONF_BAUDRATE,
CONF_BYTESIZE,
CONF_CLIMATES,
CONF_CLOSE_COMM_ON_ERROR,
CONF_DATA_TYPE,
CONF_DEVICE_ADDRESS,
CONF_FAN_MODE_HIGH,
CONF_FAN_MODE_OFF,
CONF_FAN_MODE_ON,
CONF_FAN_MODE_REGISTER,
CONF_FAN_MODE_VALUES,
CONF_HVAC_MODE_COOL,
CONF_HVAC_MODE_HEAT,
CONF_HVAC_MODE_REGISTER,
CONF_HVAC_MODE_VALUES,
CONF_INPUT_TYPE,
CONF_MSG_WAIT,
CONF_PARITY,
@ -53,6 +63,7 @@ from homeassistant.components.modbus.const import (
CONF_SWAP_BYTE,
CONF_SWAP_WORD,
CONF_SWAP_WORD_BYTE,
CONF_TARGET_TEMP,
CONF_VIRTUAL_COUNT,
DEFAULT_SCAN_INTERVAL,
MODBUS_DOMAIN as DOMAIN,
@ -68,6 +79,7 @@ from homeassistant.components.modbus.const import (
)
from homeassistant.components.modbus.validators import (
duplicate_entity_validator,
duplicate_fan_mode_validator,
duplicate_modbus_validator,
nan_validator,
number_validator,
@ -361,6 +373,25 @@ async def test_duplicate_modbus_validator(do_config) -> None:
assert len(do_config) == 1
@pytest.mark.parametrize(
"do_config",
[
{
CONF_ADDRESS: 11,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_ON: 7,
CONF_FAN_MODE_OFF: 9,
CONF_FAN_MODE_HIGH: 9,
},
}
],
)
async def test_duplicate_fan_mode_validator(do_config) -> None:
"""Test duplicate modbus validator."""
duplicate_fan_mode_validator(do_config)
assert len(do_config[CONF_FAN_MODE_VALUES]) == 2
@pytest.mark.parametrize(
"do_config",
[
@ -404,12 +435,170 @@ async def test_duplicate_modbus_validator(do_config) -> None:
],
}
],
[
{
CONF_NAME: TEST_MODBUS_NAME,
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 117,
CONF_SLAVE: 0,
},
{
CONF_NAME: TEST_ENTITY_NAME + " 2",
CONF_ADDRESS: 117,
CONF_SLAVE: 0,
},
],
}
],
],
)
async def test_duplicate_entity_validator(do_config) -> None:
"""Test duplicate entity validator."""
duplicate_entity_validator(do_config)
assert len(do_config[0][CONF_SENSORS]) == 1
if CONF_SENSORS in do_config[0]:
assert len(do_config[0][CONF_SENSORS]) == 1
elif CONF_CLIMATES in do_config[0]:
assert len(do_config[0][CONF_CLIMATES]) == 1
@pytest.mark.parametrize(
"do_config",
[
[
{
CONF_NAME: TEST_MODBUS_NAME,
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 117,
CONF_SLAVE: 0,
},
{
CONF_NAME: TEST_ENTITY_NAME + " 2",
CONF_ADDRESS: 117,
CONF_SLAVE: 0,
},
],
}
],
[
{
CONF_NAME: TEST_MODBUS_NAME,
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 118,
CONF_SLAVE: 0,
CONF_HVAC_MODE_REGISTER: {
CONF_ADDRESS: 119,
CONF_HVAC_MODE_VALUES: {
CONF_HVAC_MODE_COOL: 0,
CONF_HVAC_MODE_HEAT: 1,
},
},
},
{
CONF_NAME: TEST_ENTITY_NAME + " 2",
CONF_ADDRESS: 117,
CONF_SLAVE: 0,
CONF_HVAC_MODE_REGISTER: {
CONF_ADDRESS: 118,
CONF_HVAC_MODE_VALUES: {
CONF_HVAC_MODE_COOL: 0,
CONF_HVAC_MODE_HEAT: 1,
},
},
},
],
}
],
[
{
CONF_NAME: TEST_MODBUS_NAME,
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 117,
CONF_SLAVE: 0,
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_SLAVE: 0,
CONF_TARGET_TEMP: 99,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 120,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_ON: 0,
CONF_FAN_MODE_HIGH: 1,
},
},
},
],
}
],
[
{
CONF_NAME: TEST_MODBUS_NAME,
CONF_TYPE: TCP,
CONF_HOST: TEST_MODBUS_HOST,
CONF_PORT: TEST_PORT_TCP,
CONF_CLIMATES: [
{
CONF_NAME: TEST_ENTITY_NAME,
CONF_ADDRESS: 117,
CONF_SLAVE: 0,
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_SLAVE: 0,
CONF_TARGET_TEMP: 117,
CONF_FAN_MODE_REGISTER: {
CONF_ADDRESS: 121,
CONF_FAN_MODE_VALUES: {
CONF_FAN_MODE_ON: 0,
CONF_FAN_MODE_HIGH: 1,
},
},
},
],
}
],
],
)
async def test_duplicate_entity_validator_with_climate(do_config) -> None:
"""Test duplicate entity validator."""
duplicate_entity_validator(do_config)
assert len(do_config[0][CONF_CLIMATES]) == 1
@pytest.mark.parametrize(