mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add modbus climate hvac action (#139864)
* Added the hvac action attribute for modbus climate entities. * Fixed issue in hvac action unit test, was incorrectly referencing the hvac mode attribute. * Fixed the modbus climate test for hvac action, it now correctly checks that hvac actions in the config match HVACActions. * Made changes recommended by @crug80 to remove dead code and to add ability to use input or holding register for hvac action. * Moved action test case in test_climate.py * Updated comment for `test_service_climate_action_update` * Fixed ruff formatting error. * Addressed request to update labels from `state_*` to `action_*`
This commit is contained in:
parent
ad126a745a
commit
95afebceb4
@ -79,6 +79,16 @@ from .const import (
|
|||||||
CONF_FAN_MODE_TOP,
|
CONF_FAN_MODE_TOP,
|
||||||
CONF_FAN_MODE_VALUES,
|
CONF_FAN_MODE_VALUES,
|
||||||
CONF_FANS,
|
CONF_FANS,
|
||||||
|
CONF_HVAC_ACTION_COOLING,
|
||||||
|
CONF_HVAC_ACTION_DEFROSTING,
|
||||||
|
CONF_HVAC_ACTION_DRYING,
|
||||||
|
CONF_HVAC_ACTION_FAN,
|
||||||
|
CONF_HVAC_ACTION_HEATING,
|
||||||
|
CONF_HVAC_ACTION_IDLE,
|
||||||
|
CONF_HVAC_ACTION_OFF,
|
||||||
|
CONF_HVAC_ACTION_PREHEATING,
|
||||||
|
CONF_HVAC_ACTION_REGISTER,
|
||||||
|
CONF_HVAC_ACTION_VALUES,
|
||||||
CONF_HVAC_MODE_AUTO,
|
CONF_HVAC_MODE_AUTO,
|
||||||
CONF_HVAC_MODE_COOL,
|
CONF_HVAC_MODE_COOL,
|
||||||
CONF_HVAC_MODE_DRY,
|
CONF_HVAC_MODE_DRY,
|
||||||
@ -297,6 +307,45 @@ CLIMATE_SCHEMA = vol.All(
|
|||||||
vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean,
|
vol.Optional(CONF_WRITE_REGISTERS, default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
vol.Optional(CONF_HVAC_ACTION_REGISTER): vol.Maybe(
|
||||||
|
{
|
||||||
|
CONF_ADDRESS: cv.positive_int,
|
||||||
|
CONF_HVAC_ACTION_VALUES: {
|
||||||
|
vol.Optional(CONF_HVAC_ACTION_COOLING): vol.Any(
|
||||||
|
cv.positive_int, [cv.positive_int]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_HVAC_ACTION_DEFROSTING): vol.Any(
|
||||||
|
cv.positive_int, [cv.positive_int]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_HVAC_ACTION_DRYING): vol.Any(
|
||||||
|
cv.positive_int, [cv.positive_int]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_HVAC_ACTION_FAN): vol.Any(
|
||||||
|
cv.positive_int, [cv.positive_int]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_HVAC_ACTION_HEATING): vol.Any(
|
||||||
|
cv.positive_int, [cv.positive_int]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_HVAC_ACTION_IDLE): vol.Any(
|
||||||
|
cv.positive_int, [cv.positive_int]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_HVAC_ACTION_OFF): vol.Any(
|
||||||
|
cv.positive_int, [cv.positive_int]
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_HVAC_ACTION_PREHEATING): vol.Any(
|
||||||
|
cv.positive_int, [cv.positive_int]
|
||||||
|
),
|
||||||
|
},
|
||||||
|
vol.Optional(
|
||||||
|
CONF_INPUT_TYPE, default=CALL_TYPE_REGISTER_HOLDING
|
||||||
|
): vol.In(
|
||||||
|
[
|
||||||
|
CALL_TYPE_REGISTER_HOLDING,
|
||||||
|
CALL_TYPE_REGISTER_INPUT,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
vol.Optional(CONF_FAN_MODE_REGISTER): vol.Maybe(
|
vol.Optional(CONF_FAN_MODE_REGISTER): vol.Maybe(
|
||||||
vol.All(
|
vol.All(
|
||||||
{
|
{
|
||||||
|
@ -24,6 +24,7 @@ from homeassistant.components.climate import (
|
|||||||
SWING_VERTICAL,
|
SWING_VERTICAL,
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
ClimateEntityFeature,
|
ClimateEntityFeature,
|
||||||
|
HVACAction,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -61,6 +62,16 @@ from .const import (
|
|||||||
CONF_FAN_MODE_REGISTER,
|
CONF_FAN_MODE_REGISTER,
|
||||||
CONF_FAN_MODE_TOP,
|
CONF_FAN_MODE_TOP,
|
||||||
CONF_FAN_MODE_VALUES,
|
CONF_FAN_MODE_VALUES,
|
||||||
|
CONF_HVAC_ACTION_COOLING,
|
||||||
|
CONF_HVAC_ACTION_DEFROSTING,
|
||||||
|
CONF_HVAC_ACTION_DRYING,
|
||||||
|
CONF_HVAC_ACTION_FAN,
|
||||||
|
CONF_HVAC_ACTION_HEATING,
|
||||||
|
CONF_HVAC_ACTION_IDLE,
|
||||||
|
CONF_HVAC_ACTION_OFF,
|
||||||
|
CONF_HVAC_ACTION_PREHEATING,
|
||||||
|
CONF_HVAC_ACTION_REGISTER,
|
||||||
|
CONF_HVAC_ACTION_VALUES,
|
||||||
CONF_HVAC_MODE_AUTO,
|
CONF_HVAC_MODE_AUTO,
|
||||||
CONF_HVAC_MODE_COOL,
|
CONF_HVAC_MODE_COOL,
|
||||||
CONF_HVAC_MODE_DRY,
|
CONF_HVAC_MODE_DRY,
|
||||||
@ -74,6 +85,7 @@ from .const import (
|
|||||||
CONF_HVAC_ON_VALUE,
|
CONF_HVAC_ON_VALUE,
|
||||||
CONF_HVAC_ONOFF_COIL,
|
CONF_HVAC_ONOFF_COIL,
|
||||||
CONF_HVAC_ONOFF_REGISTER,
|
CONF_HVAC_ONOFF_REGISTER,
|
||||||
|
CONF_INPUT_TYPE,
|
||||||
CONF_MAX_TEMP,
|
CONF_MAX_TEMP,
|
||||||
CONF_MIN_TEMP,
|
CONF_MIN_TEMP,
|
||||||
CONF_STEP,
|
CONF_STEP,
|
||||||
@ -188,6 +200,34 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
|||||||
self._attr_hvac_mode = HVACMode.AUTO
|
self._attr_hvac_mode = HVACMode.AUTO
|
||||||
self._attr_hvac_modes = [HVACMode.AUTO]
|
self._attr_hvac_modes = [HVACMode.AUTO]
|
||||||
|
|
||||||
|
if CONF_HVAC_ACTION_REGISTER in config:
|
||||||
|
action_config = config[CONF_HVAC_ACTION_REGISTER]
|
||||||
|
self._hvac_action_register = action_config[CONF_ADDRESS]
|
||||||
|
self._hvac_action_type = action_config[CONF_INPUT_TYPE]
|
||||||
|
|
||||||
|
self._attr_hvac_action = None
|
||||||
|
self._hvac_action_mapping: list[tuple[int, HVACAction]] = []
|
||||||
|
action_value_config = action_config[CONF_HVAC_ACTION_VALUES]
|
||||||
|
|
||||||
|
for hvac_action_kw, hvac_action in (
|
||||||
|
(CONF_HVAC_ACTION_COOLING, HVACAction.COOLING),
|
||||||
|
(CONF_HVAC_ACTION_DEFROSTING, HVACAction.DEFROSTING),
|
||||||
|
(CONF_HVAC_ACTION_DRYING, HVACAction.DRYING),
|
||||||
|
(CONF_HVAC_ACTION_FAN, HVACAction.FAN),
|
||||||
|
(CONF_HVAC_ACTION_HEATING, HVACAction.HEATING),
|
||||||
|
(CONF_HVAC_ACTION_IDLE, HVACAction.IDLE),
|
||||||
|
(CONF_HVAC_ACTION_OFF, HVACAction.OFF),
|
||||||
|
(CONF_HVAC_ACTION_PREHEATING, HVACAction.PREHEATING),
|
||||||
|
):
|
||||||
|
if hvac_action_kw in action_value_config:
|
||||||
|
values = action_value_config[hvac_action_kw]
|
||||||
|
if not isinstance(values, list):
|
||||||
|
values = [values]
|
||||||
|
for value in values:
|
||||||
|
self._hvac_action_mapping.append((value, hvac_action))
|
||||||
|
else:
|
||||||
|
self._hvac_action_register = None
|
||||||
|
|
||||||
if CONF_FAN_MODE_REGISTER in config:
|
if CONF_FAN_MODE_REGISTER in config:
|
||||||
self._attr_supported_features = (
|
self._attr_supported_features = (
|
||||||
self._attr_supported_features | ClimateEntityFeature.FAN_MODE
|
self._attr_supported_features | ClimateEntityFeature.FAN_MODE
|
||||||
@ -216,7 +256,6 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
|||||||
self._fan_mode_mapping_from_modbus[value] = fan_mode
|
self._fan_mode_mapping_from_modbus[value] = fan_mode
|
||||||
self._fan_mode_mapping_to_modbus[fan_mode] = value
|
self._fan_mode_mapping_to_modbus[fan_mode] = value
|
||||||
self._attr_fan_modes.append(fan_mode)
|
self._attr_fan_modes.append(fan_mode)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# No FAN modes defined
|
# No FAN modes defined
|
||||||
self._fan_mode_register = None
|
self._fan_mode_register = None
|
||||||
@ -457,6 +496,20 @@ class ModbusThermostat(BaseStructPlatform, RestoreEntity, ClimateEntity):
|
|||||||
self._attr_hvac_mode = mode
|
self._attr_hvac_mode = mode
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Read the HVAC action register if defined
|
||||||
|
if self._hvac_action_register is not None:
|
||||||
|
hvac_action = await self._async_read_register(
|
||||||
|
self._hvac_action_type, self._hvac_action_register, raw=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Translate the value received
|
||||||
|
if hvac_action is not None:
|
||||||
|
self._attr_hvac_action = None
|
||||||
|
for value, action in self._hvac_action_mapping:
|
||||||
|
if hvac_action == value:
|
||||||
|
self._attr_hvac_action = action
|
||||||
|
break
|
||||||
|
|
||||||
# Read the Fan mode register if defined
|
# Read the Fan mode register if defined
|
||||||
if self._fan_mode_register is not None:
|
if self._fan_mode_register is not None:
|
||||||
fan_mode = await self._async_read_register(
|
fan_mode = await self._async_read_register(
|
||||||
|
@ -63,6 +63,16 @@ CONF_HVAC_ONOFF_REGISTER = "hvac_onoff_register"
|
|||||||
CONF_HVAC_ON_VALUE = "hvac_on_value"
|
CONF_HVAC_ON_VALUE = "hvac_on_value"
|
||||||
CONF_HVAC_OFF_VALUE = "hvac_off_value"
|
CONF_HVAC_OFF_VALUE = "hvac_off_value"
|
||||||
CONF_HVAC_ONOFF_COIL = "hvac_onoff_coil"
|
CONF_HVAC_ONOFF_COIL = "hvac_onoff_coil"
|
||||||
|
CONF_HVAC_ACTION_REGISTER = "hvac_action_register"
|
||||||
|
CONF_HVAC_ACTION_COOLING = "action_cooling"
|
||||||
|
CONF_HVAC_ACTION_DEFROSTING = "action_defrosting"
|
||||||
|
CONF_HVAC_ACTION_DRYING = "action_drying"
|
||||||
|
CONF_HVAC_ACTION_FAN = "action_fan"
|
||||||
|
CONF_HVAC_ACTION_HEATING = "action_heating"
|
||||||
|
CONF_HVAC_ACTION_IDLE = "action_idle"
|
||||||
|
CONF_HVAC_ACTION_OFF = "action_off"
|
||||||
|
CONF_HVAC_ACTION_PREHEATING = "action_preheating"
|
||||||
|
CONF_HVAC_ACTION_VALUES = "values"
|
||||||
CONF_HVAC_MODE_OFF = "state_off"
|
CONF_HVAC_MODE_OFF = "state_off"
|
||||||
CONF_HVAC_MODE_HEAT = "state_heat"
|
CONF_HVAC_MODE_HEAT = "state_heat"
|
||||||
CONF_HVAC_MODE_COOL = "state_cool"
|
CONF_HVAC_MODE_COOL = "state_cool"
|
||||||
|
@ -5,6 +5,7 @@ import pytest
|
|||||||
from homeassistant.components.climate import (
|
from homeassistant.components.climate import (
|
||||||
ATTR_FAN_MODE,
|
ATTR_FAN_MODE,
|
||||||
ATTR_FAN_MODES,
|
ATTR_FAN_MODES,
|
||||||
|
ATTR_HVAC_ACTION,
|
||||||
ATTR_HVAC_MODE,
|
ATTR_HVAC_MODE,
|
||||||
ATTR_HVAC_MODES,
|
ATTR_HVAC_MODES,
|
||||||
ATTR_SWING_MODE,
|
ATTR_SWING_MODE,
|
||||||
@ -31,6 +32,7 @@ from homeassistant.components.climate import (
|
|||||||
SWING_OFF,
|
SWING_OFF,
|
||||||
SWING_ON,
|
SWING_ON,
|
||||||
SWING_VERTICAL,
|
SWING_VERTICAL,
|
||||||
|
HVACAction,
|
||||||
HVACMode,
|
HVACMode,
|
||||||
)
|
)
|
||||||
from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY
|
from homeassistant.components.homeassistant import SERVICE_UPDATE_ENTITY
|
||||||
@ -47,6 +49,16 @@ from homeassistant.components.modbus.const import (
|
|||||||
CONF_FAN_MODE_REGISTER,
|
CONF_FAN_MODE_REGISTER,
|
||||||
CONF_FAN_MODE_TOP,
|
CONF_FAN_MODE_TOP,
|
||||||
CONF_FAN_MODE_VALUES,
|
CONF_FAN_MODE_VALUES,
|
||||||
|
CONF_HVAC_ACTION_COOLING,
|
||||||
|
CONF_HVAC_ACTION_DEFROSTING,
|
||||||
|
CONF_HVAC_ACTION_DRYING,
|
||||||
|
CONF_HVAC_ACTION_FAN,
|
||||||
|
CONF_HVAC_ACTION_HEATING,
|
||||||
|
CONF_HVAC_ACTION_IDLE,
|
||||||
|
CONF_HVAC_ACTION_OFF,
|
||||||
|
CONF_HVAC_ACTION_PREHEATING,
|
||||||
|
CONF_HVAC_ACTION_REGISTER,
|
||||||
|
CONF_HVAC_ACTION_VALUES,
|
||||||
CONF_HVAC_MODE_AUTO,
|
CONF_HVAC_MODE_AUTO,
|
||||||
CONF_HVAC_MODE_COOL,
|
CONF_HVAC_MODE_COOL,
|
||||||
CONF_HVAC_MODE_DRY,
|
CONF_HVAC_MODE_DRY,
|
||||||
@ -224,6 +236,43 @@ ENTITY_ID = f"{CLIMATE_DOMAIN}.{TEST_ENTITY_NAME}".replace(" ", "_")
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 117,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_HVAC_ONOFF_REGISTER: 12,
|
||||||
|
CONF_HVAC_MODE_REGISTER: {
|
||||||
|
CONF_ADDRESS: 11,
|
||||||
|
CONF_WRITE_REGISTERS: True,
|
||||||
|
CONF_HVAC_MODE_VALUES: {
|
||||||
|
CONF_HVAC_MODE_OFF: 0,
|
||||||
|
CONF_HVAC_MODE_HEAT: 1,
|
||||||
|
CONF_HVAC_MODE_COOL: 2,
|
||||||
|
CONF_HVAC_MODE_HEAT_COOL: 3,
|
||||||
|
CONF_HVAC_MODE_DRY: 4,
|
||||||
|
CONF_HVAC_MODE_FAN_ONLY: 5,
|
||||||
|
CONF_HVAC_MODE_AUTO: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
CONF_HVAC_ACTION_REGISTER: {
|
||||||
|
CONF_ADDRESS: 14,
|
||||||
|
CONF_HVAC_ACTION_VALUES: {
|
||||||
|
CONF_HVAC_ACTION_COOLING: 0,
|
||||||
|
CONF_HVAC_ACTION_DEFROSTING: 1,
|
||||||
|
CONF_HVAC_ACTION_DRYING: 2,
|
||||||
|
CONF_HVAC_ACTION_FAN: 3,
|
||||||
|
CONF_HVAC_ACTION_HEATING: 4,
|
||||||
|
CONF_HVAC_ACTION_IDLE: 5,
|
||||||
|
CONF_HVAC_ACTION_OFF: 6,
|
||||||
|
CONF_HVAC_ACTION_PREHEATING: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_config_climate(hass: HomeAssistant, mock_modbus) -> None:
|
async def test_config_climate(hass: HomeAssistant, mock_modbus) -> None:
|
||||||
@ -745,6 +794,95 @@ async def test_hvac_onoff_coil_update(
|
|||||||
assert state.state == result
|
assert state.state == result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("do_config", "result", "register_words"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 116,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_SCAN_INTERVAL: 0,
|
||||||
|
CONF_DATA_TYPE: DataType.INT32,
|
||||||
|
CONF_HVAC_ACTION_REGISTER: {
|
||||||
|
CONF_ADDRESS: 118,
|
||||||
|
CONF_HVAC_ACTION_VALUES: {
|
||||||
|
CONF_HVAC_ACTION_IDLE: 0,
|
||||||
|
CONF_HVAC_ACTION_HEATING: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
HVACAction.HEATING,
|
||||||
|
[0x01],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 116,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_SCAN_INTERVAL: 0,
|
||||||
|
CONF_DATA_TYPE: DataType.INT32,
|
||||||
|
CONF_HVAC_ACTION_REGISTER: {
|
||||||
|
CONF_ADDRESS: 118,
|
||||||
|
CONF_HVAC_ACTION_VALUES: {
|
||||||
|
CONF_HVAC_ACTION_COOLING: 0,
|
||||||
|
CONF_HVAC_ACTION_HEATING: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
HVACAction.COOLING,
|
||||||
|
[0x00],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
{
|
||||||
|
CONF_CLIMATES: [
|
||||||
|
{
|
||||||
|
CONF_NAME: TEST_ENTITY_NAME,
|
||||||
|
CONF_TARGET_TEMP: 116,
|
||||||
|
CONF_ADDRESS: 117,
|
||||||
|
CONF_SLAVE: 10,
|
||||||
|
CONF_SCAN_INTERVAL: 0,
|
||||||
|
CONF_DATA_TYPE: DataType.INT32,
|
||||||
|
CONF_HVAC_ACTION_REGISTER: {
|
||||||
|
CONF_ADDRESS: 118,
|
||||||
|
CONF_HVAC_ACTION_VALUES: {
|
||||||
|
CONF_HVAC_ACTION_OFF: 0,
|
||||||
|
CONF_HVAC_ACTION_DRYING: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
HVACAction.DRYING,
|
||||||
|
[0x01],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_service_climate_action_update(
|
||||||
|
hass: HomeAssistant, mock_modbus_ha, result, register_words
|
||||||
|
) -> None:
|
||||||
|
"""Test HVAC action updates."""
|
||||||
|
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()
|
||||||
|
assert hass.states.get(ENTITY_ID).attributes[ATTR_HVAC_ACTION] == result
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("do_config", "result", "register_words"),
|
("do_config", "result", "register_words"),
|
||||||
[
|
[
|
||||||
|
Loading…
x
Reference in New Issue
Block a user