1
mirror of https://github.com/home-assistant/core synced 2024-07-15 09:42:11 +02:00

Handle Auto Fan and MyFan in Advantage Air (#95594)

* Decouple climate from MyFan

* Add tests

* Prepare for auto change

* Handle both modes

* Fix import

* Remove reference to FAN map

* Correct auto fan logic

* Remove multiline ternary operator

Co-authored-by: G Johansson <goran.johansson@shiftit.se>

* Fix coverage

* fix tests

* ruff

* Test auto fan mode with snapshot

* add more testing

* Add switch testing

* Fix a bug caught by new tests

* Remove ineffective snapshot tests

* Readd snapshots but use args

---------

Co-authored-by: G Johansson <goran.johansson@shiftit.se>
This commit is contained in:
Brett Adams 2023-12-27 08:31:25 +10:00 committed by GitHub
parent 615cd56f03
commit 9dde42a023
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 197 additions and 46 deletions

View File

@ -21,6 +21,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
ADVANTAGE_AIR_AUTOFAN_ENABLED,
ADVANTAGE_AIR_STATE_CLOSE,
ADVANTAGE_AIR_STATE_OFF,
ADVANTAGE_AIR_STATE_ON,
@ -39,16 +40,6 @@ ADVANTAGE_AIR_HVAC_MODES = {
}
HASS_HVAC_MODES = {v: k for k, v in ADVANTAGE_AIR_HVAC_MODES.items()}
ADVANTAGE_AIR_FAN_MODES = {
"autoAA": FAN_AUTO,
"low": FAN_LOW,
"medium": FAN_MEDIUM,
"high": FAN_HIGH,
}
HASS_FAN_MODES = {v: k for k, v in ADVANTAGE_AIR_FAN_MODES.items()}
FAN_SPEEDS = {FAN_LOW: 30, FAN_MEDIUM: 60, FAN_HIGH: 100}
ADVANTAGE_AIR_AUTOFAN = "aaAutoFanModeEnabled"
ADVANTAGE_AIR_MYZONE = "MyZone"
ADVANTAGE_AIR_MYAUTO = "MyAuto"
ADVANTAGE_AIR_MYAUTO_ENABLED = "myAutoModeEnabled"
@ -56,6 +47,7 @@ ADVANTAGE_AIR_MYTEMP = "MyTemp"
ADVANTAGE_AIR_MYTEMP_ENABLED = "climateControlModeEnabled"
ADVANTAGE_AIR_HEAT_TARGET = "myAutoHeatTargetTemp"
ADVANTAGE_AIR_COOL_TARGET = "myAutoCoolTargetTemp"
ADVANTAGE_AIR_MYFAN = "autoAA"
PARALLEL_UPDATES = 0
@ -85,27 +77,25 @@ async def async_setup_entry(
class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
"""AdvantageAir AC unit."""
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH]
_attr_fan_modes = [FAN_LOW, FAN_MEDIUM, FAN_HIGH, FAN_AUTO]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = PRECISION_WHOLE
_attr_max_temp = 32
_attr_min_temp = 16
_attr_name = None
_attr_hvac_modes = [
HVACMode.OFF,
HVACMode.COOL,
HVACMode.HEAT,
HVACMode.FAN_ONLY,
HVACMode.DRY,
]
_attr_supported_features = ClimateEntityFeature.FAN_MODE
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
"""Initialize an AdvantageAir AC unit."""
super().__init__(instance, ac_key)
self._attr_supported_features = ClimateEntityFeature.FAN_MODE
self._attr_hvac_modes = [
HVACMode.OFF,
HVACMode.COOL,
HVACMode.HEAT,
HVACMode.FAN_ONLY,
HVACMode.DRY,
]
# Set supported features and HVAC modes based on current operating mode
if self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED):
# MyAuto
@ -118,10 +108,6 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
# MyZone
self._attr_supported_features |= ClimateEntityFeature.TARGET_TEMPERATURE
# Add "ezfan" mode if supported
if self._ac.get(ADVANTAGE_AIR_AUTOFAN):
self._attr_fan_modes += [FAN_AUTO]
@property
def current_temperature(self) -> float | None:
"""Return the selected zones current temperature."""
@ -151,7 +137,7 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
@property
def fan_mode(self) -> str | None:
"""Return the current fan modes."""
return ADVANTAGE_AIR_FAN_MODES.get(self._ac["fan"])
return FAN_AUTO if self._ac["fan"] == ADVANTAGE_AIR_MYFAN else self._ac["fan"]
@property
def target_temperature_high(self) -> float | None:
@ -189,7 +175,11 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the Fan Mode."""
await self.async_update_ac({"fan": HASS_FAN_MODES.get(fan_mode)})
if fan_mode == FAN_AUTO and self._ac.get(ADVANTAGE_AIR_AUTOFAN_ENABLED):
mode = ADVANTAGE_AIR_MYFAN
else:
mode = fan_mode
await self.async_update_ac({"fan": mode})
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the Temperature."""

View File

@ -5,3 +5,4 @@ ADVANTAGE_AIR_STATE_OPEN = "open"
ADVANTAGE_AIR_STATE_CLOSE = "close"
ADVANTAGE_AIR_STATE_ON = "on"
ADVANTAGE_AIR_STATE_OFF = "off"
ADVANTAGE_AIR_AUTOFAN_ENABLED = "aaAutoFanModeEnabled"

View File

@ -7,6 +7,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
ADVANTAGE_AIR_AUTOFAN_ENABLED,
ADVANTAGE_AIR_STATE_OFF,
ADVANTAGE_AIR_STATE_ON,
DOMAIN as ADVANTAGE_AIR_DOMAIN,
@ -29,6 +30,8 @@ async def async_setup_entry(
for ac_key, ac_device in aircons.items():
if ac_device["info"]["freshAirStatus"] != "none":
entities.append(AdvantageAirFreshAir(instance, ac_key))
if ADVANTAGE_AIR_AUTOFAN_ENABLED in ac_device["info"]:
entities.append(AdvantageAirMyFan(instance, ac_key))
if things := instance.coordinator.data.get("myThings"):
for thing in things["things"].values():
if thing["channelDipState"] == 8: # 8 = Other relay
@ -62,6 +65,32 @@ class AdvantageAirFreshAir(AdvantageAirAcEntity, SwitchEntity):
await self.async_update_ac({"freshAirStatus": ADVANTAGE_AIR_STATE_OFF})
class AdvantageAirMyFan(AdvantageAirAcEntity, SwitchEntity):
"""Representation of Advantage Air MyFan control."""
_attr_icon = "mdi:fan-auto"
_attr_name = "MyFan"
_attr_device_class = SwitchDeviceClass.SWITCH
def __init__(self, instance: AdvantageAirData, ac_key: str) -> None:
"""Initialize an Advantage Air MyFan control."""
super().__init__(instance, ac_key)
self._attr_unique_id += "-myfan"
@property
def is_on(self) -> bool:
"""Return the MyFan status."""
return self._ac[ADVANTAGE_AIR_AUTOFAN_ENABLED]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn MyFan on."""
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: True})
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn MyFan off."""
await self.async_update_ac({ADVANTAGE_AIR_AUTOFAN_ENABLED: False})
class AdvantageAirRelay(AdvantageAirThingEntity, SwitchEntity):
"""Representation of Advantage Air Thing."""

View File

@ -0,0 +1,55 @@
# serializer version: 1
# name: test_climate_myauto_main[climate.myauto-fanmode]
dict({
'ac3': dict({
'info': dict({
'fan': 'autoAA',
}),
}),
})
# ---
# name: test_climate_myauto_main[climate.myauto-settemp]
dict({
'ac3': dict({
'info': dict({
'myAutoCoolTargetTemp': 23.0,
'myAutoHeatTargetTemp': 21.0,
}),
}),
})
# ---
# name: test_climate_myauto_main[climate.myauto]
StateSnapshot({
'attributes': ReadOnlyDict({
'current_temperature': None,
'fan_mode': 'auto',
'fan_modes': list([
'low',
'medium',
'high',
'auto',
]),
'friendly_name': 'myauto',
'hvac_modes': list([
<HVACMode.OFF: 'off'>,
<HVACMode.COOL: 'cool'>,
<HVACMode.HEAT: 'heat'>,
<HVACMode.FAN_ONLY: 'fan_only'>,
<HVACMode.DRY: 'dry'>,
<HVACMode.HEAT_COOL: 'heat_cool'>,
]),
'max_temp': 32,
'min_temp': 16,
'supported_features': <ClimateEntityFeature: 11>,
'target_temp_high': 24,
'target_temp_low': 20,
'target_temp_step': 1,
'temperature': 24,
}),
'context': <ANY>,
'entity_id': 'climate.myauto',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'heat_cool',
})
# ---

View File

@ -0,0 +1,33 @@
# serializer version: 1
# name: test_cover_async_setup_entry[switch.myzone_myfan-turnoff]
dict({
'ac1': dict({
'info': dict({
'aaAutoFanModeEnabled': False,
}),
}),
})
# ---
# name: test_cover_async_setup_entry[switch.myzone_myfan-turnon]
dict({
'ac1': dict({
'info': dict({
'aaAutoFanModeEnabled': True,
}),
}),
})
# ---
# name: test_cover_async_setup_entry[switch.myzone_myfan]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'switch',
'friendly_name': 'myzone MyFan',
'icon': 'mdi:fan-auto',
}),
'context': <ANY>,
'entity_id': 'switch.myzone_myfan',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -4,6 +4,7 @@ from unittest.mock import AsyncMock
from advantage_air import ApiError
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.climate import (
ATTR_CURRENT_TEMPERATURE,
@ -14,6 +15,7 @@ from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH,
ATTR_TARGET_TEMP_LOW,
DOMAIN as CLIMATE_DOMAIN,
FAN_AUTO,
FAN_LOW,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
@ -27,7 +29,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import add_mock_config, patch_update
from . import add_mock_config
async def test_climate_myzone_main(
@ -182,6 +184,7 @@ async def test_climate_myauto_main(
entity_registry: er.EntityRegistry,
mock_get: AsyncMock,
mock_update: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test climate platform zone entity."""
@ -189,27 +192,35 @@ async def test_climate_myauto_main(
# Test MyAuto Climate Entity
entity_id = "climate.myauto"
state = hass.states.get(entity_id)
assert state
assert state.attributes.get(ATTR_TARGET_TEMP_LOW) == 20
assert state.attributes.get(ATTR_TARGET_TEMP_HIGH) == 24
assert hass.states.get(entity_id) == snapshot(name=entity_id)
entry = entity_registry.async_get(entity_id)
assert entry
assert entry.unique_id == "uniqueid-ac3"
with patch_update() as mock_update:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: [entity_id],
ATTR_TARGET_TEMP_LOW: 21,
ATTR_TARGET_TEMP_HIGH: 23,
},
blocking=True,
)
mock_update.assert_called_once()
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: [entity_id],
ATTR_TARGET_TEMP_LOW: 21,
ATTR_TARGET_TEMP_HIGH: 23,
},
blocking=True,
)
mock_update.assert_called_once()
assert mock_update.call_args[0][0] == snapshot(name=f"{entity_id}-settemp")
mock_update.reset_mock()
# Test AutoFanMode
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
{ATTR_ENTITY_ID: [entity_id], ATTR_FAN_MODE: FAN_AUTO},
blocking=True,
)
mock_update.assert_called_once()
assert mock_update.call_args[0][0] == snapshot(name=f"{entity_id}-fanmode")
async def test_climate_async_failed_update(

View File

@ -1,8 +1,9 @@
"""Test the Advantage Air Switch Platform."""
from unittest.mock import AsyncMock
from syrupy import SnapshotAssertion
from homeassistant.components.switch import (
DOMAIN as SWITCH_DOMAIN,
SERVICE_TURN_OFF,
@ -20,12 +21,15 @@ async def test_cover_async_setup_entry(
entity_registry: er.EntityRegistry,
mock_get: AsyncMock,
mock_update: AsyncMock,
snapshot: SnapshotAssertion,
) -> None:
"""Test switch platform."""
await add_mock_config(hass)
# Test Switch Entity
registry = er.async_get(hass)
# Test Fresh Air Switch Entity
entity_id = "switch.myzone_fresh_air"
state = hass.states.get(entity_id)
assert state
@ -51,6 +55,34 @@ async def test_cover_async_setup_entry(
blocking=True,
)
mock_update.assert_called_once()
mock_update.reset_mock()
# Test MyFan Switch Entity
entity_id = "switch.myzone_myfan"
assert hass.states.get(entity_id) == snapshot(name=entity_id)
entry = registry.async_get(entity_id)
assert entry
assert entry.unique_id == "uniqueid-ac1-myfan"
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
mock_update.assert_called_once()
assert mock_update.call_args[0][0] == snapshot(name=f"{entity_id}-turnon")
mock_update.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: [entity_id]},
blocking=True,
)
mock_update.assert_called_once()
assert mock_update.call_args[0][0] == snapshot(name=f"{entity_id}-turnoff")
async def test_things_switch(