mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 03:03:17 +01:00
Add Fan mode support to Control4 integration (#159980)
This commit is contained in:
@@ -38,6 +38,8 @@ CONTROL4_CURRENT_TEMPERATURE = "TEMPERATURE_F"
|
||||
CONTROL4_HUMIDITY = "HUMIDITY"
|
||||
CONTROL4_COOL_SETPOINT = "COOL_SETPOINT_F"
|
||||
CONTROL4_HEAT_SETPOINT = "HEAT_SETPOINT_F"
|
||||
CONTROL4_FAN_MODE = "FAN_MODE"
|
||||
CONTROL4_FAN_MODES_LIST = "FAN_MODES_LIST"
|
||||
|
||||
VARIABLES_OF_INTEREST = {
|
||||
CONTROL4_HVAC_STATE,
|
||||
@@ -46,6 +48,8 @@ VARIABLES_OF_INTEREST = {
|
||||
CONTROL4_HUMIDITY,
|
||||
CONTROL4_COOL_SETPOINT,
|
||||
CONTROL4_HEAT_SETPOINT,
|
||||
CONTROL4_FAN_MODE,
|
||||
CONTROL4_FAN_MODES_LIST,
|
||||
}
|
||||
|
||||
# Map Control4 HVAC modes to Home Assistant
|
||||
@@ -153,12 +157,7 @@ class Control4Climate(Control4Entity, ClimateEntity):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
_attr_translation_key = "thermostat"
|
||||
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.HEAT_COOL]
|
||||
|
||||
def __init__(
|
||||
@@ -201,6 +200,19 @@ class Control4Climate(Control4Entity, ClimateEntity):
|
||||
"""Return the thermostat data from the coordinator."""
|
||||
return self.coordinator.data.get(self._idx)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
"""Return the list of supported features."""
|
||||
features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
if self.fan_modes:
|
||||
features |= ClimateEntityFeature.FAN_MODE
|
||||
return features
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
@@ -275,6 +287,28 @@ class Control4Climate(Control4Entity, ClimateEntity):
|
||||
return data.get(CONTROL4_HEAT_SETPOINT)
|
||||
return None
|
||||
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the current fan mode."""
|
||||
data = self._thermostat_data
|
||||
if data is None:
|
||||
return None
|
||||
c4_fan_mode = data.get(CONTROL4_FAN_MODE)
|
||||
if c4_fan_mode is None:
|
||||
return None
|
||||
return c4_fan_mode.lower()
|
||||
|
||||
@property
|
||||
def fan_modes(self) -> list[str] | None:
|
||||
"""Return the list of available fan modes."""
|
||||
data = self._thermostat_data
|
||||
if data is None:
|
||||
return None
|
||||
modes = data.get(CONTROL4_FAN_MODES_LIST)
|
||||
if not modes:
|
||||
return None
|
||||
return [m.strip().lower() for m in modes.split(",") if m.strip()]
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new target HVAC mode."""
|
||||
c4_hvac_mode = HA_TO_C4_HVAC_MODE[hvac_mode]
|
||||
@@ -303,3 +337,9 @@ class Control4Climate(Control4Entity, ClimateEntity):
|
||||
await c4_climate.setHeatSetpointF(temp)
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new target fan mode."""
|
||||
c4_climate = self._create_api_object()
|
||||
await c4_climate.setFanMode(fan_mode.title())
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
15
homeassistant/components/control4/icons.json
Normal file
15
homeassistant/components/control4/icons.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"entity": {
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"circulate": "mdi:fan-clock"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"circulate": "Circulate"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -127,6 +127,8 @@ def mock_climate_variables() -> dict:
|
||||
"HUMIDITY": 45,
|
||||
"COOL_SETPOINT_F": 75.0,
|
||||
"HEAT_SETPOINT_F": 68.0,
|
||||
"FAN_MODE": "Auto",
|
||||
"FAN_MODES_LIST": "Auto,On,Circulate",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +159,7 @@ def mock_c4_climate() -> Generator[MagicMock]:
|
||||
mock_instance.setHvacMode = AsyncMock()
|
||||
mock_instance.setHeatSetpointF = AsyncMock()
|
||||
mock_instance.setCoolSetpointF = AsyncMock()
|
||||
mock_instance.setFanMode = AsyncMock()
|
||||
yield mock_instance
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,11 @@
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'fan_modes': list([
|
||||
'auto',
|
||||
'on',
|
||||
'circulate',
|
||||
]),
|
||||
'hvac_modes': list([
|
||||
<HVACMode.OFF: 'off'>,
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
@@ -38,8 +43,8 @@
|
||||
'platform': 'control4',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 387>,
|
||||
'translation_key': None,
|
||||
'supported_features': <ClimateEntityFeature: 395>,
|
||||
'translation_key': 'thermostat',
|
||||
'unique_id': '123',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
@@ -49,6 +54,12 @@
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_humidity': 45,
|
||||
'current_temperature': 72,
|
||||
'fan_mode': 'auto',
|
||||
'fan_modes': list([
|
||||
'auto',
|
||||
'on',
|
||||
'circulate',
|
||||
]),
|
||||
'friendly_name': 'Test Controller Residential Thermostat V2',
|
||||
'hvac_action': <HVACAction.OFF: 'off'>,
|
||||
'hvac_modes': list([
|
||||
@@ -59,7 +70,7 @@
|
||||
]),
|
||||
'max_temp': 95,
|
||||
'min_temp': 45,
|
||||
'supported_features': <ClimateEntityFeature: 387>,
|
||||
'supported_features': <ClimateEntityFeature: 395>,
|
||||
'target_temp_high': None,
|
||||
'target_temp_low': None,
|
||||
'temperature': 68,
|
||||
|
||||
@@ -7,13 +7,16 @@ import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_TARGET_TEMP_HIGH,
|
||||
ATTR_TARGET_TEMP_LOW,
|
||||
ATTR_TEMPERATURE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
@@ -406,3 +409,61 @@ async def test_climate_unknown_hvac_state(
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get("hvac_action") is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_c4_account",
|
||||
"mock_c4_director",
|
||||
"mock_climate_update_variables",
|
||||
"init_integration",
|
||||
)
|
||||
async def test_set_fan_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_c4_climate: MagicMock,
|
||||
) -> None:
|
||||
"""Test setting fan mode."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: "on"},
|
||||
blocking=True,
|
||||
)
|
||||
# Verify the Control4 API is called with the C4 format ("On" not "on")
|
||||
mock_c4_climate.setFanMode.assert_called_once_with("On")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mock_climate_variables",
|
||||
[
|
||||
{
|
||||
123: {
|
||||
"HVAC_STATE": "idle",
|
||||
"HVAC_MODE": "Heat",
|
||||
"TEMPERATURE_F": 72.0,
|
||||
"HUMIDITY": 50,
|
||||
"COOL_SETPOINT_F": 75.0,
|
||||
"HEAT_SETPOINT_F": 68.0,
|
||||
# No FAN_MODE or FAN_MODES_LIST
|
||||
}
|
||||
}
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures(
|
||||
"mock_c4_account",
|
||||
"mock_c4_director",
|
||||
"mock_climate_update_variables",
|
||||
"init_integration",
|
||||
)
|
||||
async def test_fan_mode_not_supported(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test fan mode feature not set when device doesn't support it."""
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get("fan_mode") is None
|
||||
assert state.attributes.get("fan_modes") is None
|
||||
assert not (
|
||||
state.attributes.get("supported_features") & ClimateEntityFeature.FAN_MODE
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user