Add presets to Advantage Air (#109485)

* Add presets

* Make hvac_modes dynamic

* Add supported features

* Fix set preset mode

* Add coverage

* Remove unused constants

* Remove the extra newline

* Add snapshot assertion to new test

* Add comments

* Use ServiceValidationError

* Test for ServiceValidationError

* Fix typo

* Refactor to use _handle_coordinator_update

* Remove preset_mode prop

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Brett Adams 2024-02-21 07:00:52 +10:00 committed by GitHub
parent 9f8e4cecdd
commit 5b00703ef7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 123 additions and 36 deletions

View File

@ -17,7 +17,8 @@ from homeassistant.components.climate import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import ( from .const import (
@ -49,6 +50,24 @@ ADVANTAGE_AIR_HEAT_TARGET = "myAutoHeatTargetTemp"
ADVANTAGE_AIR_COOL_TARGET = "myAutoCoolTargetTemp" ADVANTAGE_AIR_COOL_TARGET = "myAutoCoolTargetTemp"
ADVANTAGE_AIR_MYFAN = "autoAA" ADVANTAGE_AIR_MYFAN = "autoAA"
HVAC_MODES = [
HVACMode.OFF,
HVACMode.COOL,
HVACMode.HEAT,
HVACMode.FAN_ONLY,
HVACMode.DRY,
]
HVAC_MODES_MYAUTO = HVAC_MODES + [HVACMode.HEAT_COOL]
SUPPORTED_FEATURES = (
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
SUPPORTED_FEATURES_MYZONE = SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE
SUPPORTED_FEATURES_MYAUTO = (
SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
)
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -84,34 +103,56 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
_attr_min_temp = 16 _attr_min_temp = 16
_attr_name = None _attr_name = None
_enable_turn_on_off_backwards_compatibility = False _enable_turn_on_off_backwards_compatibility = False
_support_preset = ClimateEntityFeature(0)
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None: def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
"""Initialize an AdvantageAir AC unit.""" """Initialize an AdvantageAir AC unit."""
super().__init__(instance, ac_key) super().__init__(instance, ac_key)
self._attr_supported_features = ( self._attr_preset_modes = [ADVANTAGE_AIR_MYZONE]
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_OFF # Add "MyTemp" preset if available
| ClimateEntityFeature.TURN_ON if ADVANTAGE_AIR_MYTEMP_ENABLED in self._ac:
) self._attr_preset_modes += [ADVANTAGE_AIR_MYTEMP]
self._attr_hvac_modes = [ self._support_preset = ClimateEntityFeature.PRESET_MODE
HVACMode.OFF,
HVACMode.COOL, # Add "MyAuto" preset if available
HVACMode.HEAT, if ADVANTAGE_AIR_MYAUTO_ENABLED in self._ac:
HVACMode.FAN_ONLY, self._attr_preset_modes += [ADVANTAGE_AIR_MYAUTO]
HVACMode.DRY, self._support_preset = ClimateEntityFeature.PRESET_MODE
]
# Set supported features and HVAC modes based on current operating mode # Setup attributes based on current preset
self._async_configure_preset()
def _async_configure_preset(self) -> None:
"""Configure attributes based on preset."""
# Preset Changes
if self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED): if self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED):
# MyAuto # MyAuto
self._attr_supported_features |= ( self._attr_preset_mode = ADVANTAGE_AIR_MYAUTO
ClimateEntityFeature.TARGET_TEMPERATURE self._attr_hvac_modes = HVAC_MODES_MYAUTO
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE self._attr_supported_features = (
SUPPORTED_FEATURES_MYAUTO | self._support_preset
) )
self._attr_hvac_modes += [HVACMode.HEAT_COOL] elif self._ac.get(ADVANTAGE_AIR_MYTEMP_ENABLED):
elif not self._ac.get(ADVANTAGE_AIR_MYTEMP_ENABLED): # MyTemp
self._attr_preset_mode = ADVANTAGE_AIR_MYTEMP
self._attr_hvac_modes = HVAC_MODES
self._attr_supported_features = SUPPORTED_FEATURES | self._support_preset
else:
# MyZone # MyZone
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE self._attr_preset_mode = ADVANTAGE_AIR_MYZONE
self._attr_hvac_modes = HVAC_MODES
self._attr_supported_features = (
SUPPORTED_FEATURES_MYZONE | self._support_preset
)
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._async_configure_preset()
super()._handle_coordinator_update()
@property @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
@ -124,11 +165,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
def target_temperature(self) -> float | None: def target_temperature(self) -> float | None:
"""Return the current target temperature.""" """Return the current target temperature."""
# If the system is in MyZone mode, and a zone is set, return that temperature instead. # If the system is in MyZone mode, and a zone is set, return that temperature instead.
if ( if self._myzone and self.preset_mode == ADVANTAGE_AIR_MYZONE:
self._myzone
and not self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED)
and not self._ac.get(ADVANTAGE_AIR_MYTEMP_ENABLED)
):
return self._myzone["setTemp"] return self._myzone["setTemp"]
return self._ac["setTemp"] return self._ac["setTemp"]
@ -169,14 +206,15 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC Mode and State.""" """Set the HVAC Mode and State."""
if hvac_mode == HVACMode.OFF: if hvac_mode == HVACMode.OFF:
await self.async_update_ac({"state": ADVANTAGE_AIR_STATE_OFF}) return await self.async_turn_off()
else: if hvac_mode == HVACMode.HEAT_COOL and self.preset_mode != ADVANTAGE_AIR_MYAUTO:
await self.async_update_ac( raise ServiceValidationError("Heat/Cool is not supported in this mode")
{ await self.async_update_ac(
"state": ADVANTAGE_AIR_STATE_ON, {
"mode": HASS_HVAC_MODES.get(hvac_mode), "state": ADVANTAGE_AIR_STATE_ON,
} "mode": HASS_HVAC_MODES.get(hvac_mode),
) }
)
async def async_set_fan_mode(self, fan_mode: str) -> None: async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the Fan Mode.""" """Set the Fan Mode."""
@ -198,6 +236,16 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
} }
) )
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode."""
change = {}
if ADVANTAGE_AIR_MYTEMP_ENABLED in self._ac:
change[ADVANTAGE_AIR_MYTEMP_ENABLED] = preset_mode == ADVANTAGE_AIR_MYTEMP
if ADVANTAGE_AIR_MYAUTO_ENABLED in self._ac:
change[ADVANTAGE_AIR_MYAUTO_ENABLED] = preset_mode == ADVANTAGE_AIR_MYAUTO
if change:
await self.async_update_ac(change)
class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity): class AdvantageAirZone(AdvantageAirZoneEntity, ClimateEntity):
"""AdvantageAir MyTemp Zone control.""" """AdvantageAir MyTemp Zone control."""

View File

@ -40,11 +40,16 @@
]), ]),
'max_temp': 32, 'max_temp': 32,
'min_temp': 16, 'min_temp': 16,
'supported_features': <ClimateEntityFeature: 395>, 'preset_mode': 'MyAuto',
'preset_modes': list([
'MyZone',
'MyTemp',
'MyAuto',
]),
'supported_features': <ClimateEntityFeature: 410>,
'target_temp_high': 24, 'target_temp_high': 24,
'target_temp_low': 20, 'target_temp_low': 20,
'target_temp_step': 1, 'target_temp_step': 1,
'temperature': 24,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'climate.myauto', 'entity_id': 'climate.myauto',
@ -53,3 +58,13 @@
'state': 'heat_cool', 'state': 'heat_cool',
}) })
# --- # ---
# name: test_climate_myzone_main[climate.myzone-preset]
dict({
'ac1': dict({
'info': dict({
'climateControlModeEnabled': False,
'myAutoModeEnabled': True,
}),
}),
})
# ---

View File

@ -6,12 +6,14 @@ from advantage_air import ApiError
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.advantage_air.climate import ADVANTAGE_AIR_MYAUTO
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE, ATTR_CURRENT_TEMPERATURE,
ATTR_FAN_MODE, ATTR_FAN_MODE,
ATTR_HVAC_MODE, ATTR_HVAC_MODE,
ATTR_MAX_TEMP, ATTR_MAX_TEMP,
ATTR_MIN_TEMP, ATTR_MIN_TEMP,
ATTR_PRESET_MODE,
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_LOW,
DOMAIN as CLIMATE_DOMAIN, DOMAIN as CLIMATE_DOMAIN,
@ -19,6 +21,7 @@ from homeassistant.components.climate import (
FAN_LOW, FAN_LOW,
SERVICE_SET_FAN_MODE, SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,
SERVICE_TURN_ON, SERVICE_TURN_ON,
@ -26,7 +29,7 @@ from homeassistant.components.climate import (
) )
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from . import add_mock_config from . import add_mock_config
@ -37,6 +40,7 @@ async def test_climate_myzone_main(
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
mock_get: AsyncMock, mock_get: AsyncMock,
mock_update: AsyncMock, mock_update: AsyncMock,
snapshot: SnapshotAssertion,
) -> None: ) -> None:
"""Test climate platform main entity.""" """Test climate platform main entity."""
@ -125,6 +129,26 @@ async def test_climate_myzone_main(
mock_update.assert_called_once() mock_update.assert_called_once()
mock_update.reset_mock() mock_update.reset_mock()
# Change Preset
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: [entity_id], ATTR_PRESET_MODE: ADVANTAGE_AIR_MYAUTO},
blocking=True,
)
mock_update.assert_called_once()
assert mock_update.call_args[0][0] == snapshot(name=f"{entity_id}-preset")
mock_update.reset_mock()
# Test setting HEAT COOL when its not supported
with pytest.raises(ServiceValidationError):
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: [entity_id], ATTR_HVAC_MODE: HVACMode.HEAT_COOL},
blocking=True,
)
async def test_climate_myzone_zone( async def test_climate_myzone_zone(
hass: HomeAssistant, hass: HomeAssistant,