Add Modbus fan speed support (#104577)
Co-authored-by: jan iversen <jancasacondor@gmail.com>
This commit is contained in:
parent
22119a2fd8
commit
e051244927
6 changed files with 587 additions and 16 deletions
|
@ -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,
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Reference in a new issue