Fix evohome HVAC modes for VisionPro Wifi systems (#129161)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
David Bonnes 2024-10-29 12:37:35 +00:00 committed by GitHub
parent db4278fb9d
commit a36b350954
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 291 additions and 26 deletions

View File

@ -66,8 +66,6 @@ _LOGGER = logging.getLogger(__name__)
PRESET_RESET = "Reset" # reset all child zones to EVO_FOLLOW
PRESET_CUSTOM = "Custom"
HA_HVAC_TO_TCS = {HVACMode.OFF: EVO_HEATOFF, HVACMode.HEAT: EVO_AUTO}
TCS_PRESET_TO_HA = {
EVO_AWAY: PRESET_AWAY,
EVO_CUSTOM: PRESET_CUSTOM,
@ -150,14 +148,10 @@ async def async_setup_platform(
class EvoClimateEntity(EvoDevice, ClimateEntity):
"""Base for any evohome-compatible climate entity (controller, zone)."""
_attr_hvac_modes = [HVACMode.OFF, HVACMode.HEAT]
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_enable_turn_on_off_backwards_compatibility = False
@property
def hvac_modes(self) -> list[HVACMode]:
"""Return a list of available hvac operation modes."""
return list(HA_HVAC_TO_TCS)
class EvoZone(EvoChild, EvoClimateEntity):
"""Base for any evohome-compatible heating zone."""
@ -365,9 +359,9 @@ class EvoController(EvoClimateEntity):
self._attr_unique_id = evo_device.systemId
self._attr_name = evo_device.location.name
modes = [m[SZ_SYSTEM_MODE] for m in evo_broker.tcs.allowedSystemModes]
self._evo_modes = [m[SZ_SYSTEM_MODE] for m in evo_device.allowedSystemModes]
self._attr_preset_modes = [
TCS_PRESET_TO_HA[m] for m in modes if m in list(TCS_PRESET_TO_HA)
TCS_PRESET_TO_HA[m] for m in self._evo_modes if m in list(TCS_PRESET_TO_HA)
]
if self._attr_preset_modes:
self._attr_supported_features = ClimateEntityFeature.PRESET_MODE
@ -401,14 +395,14 @@ class EvoController(EvoClimateEntity):
"""Set a Controller to any of its native EVO_* operating modes."""
until = dt_util.as_utc(until) if until else None
await self._evo_broker.call_client_api(
self._evo_tcs.set_mode(mode, until=until) # type: ignore[arg-type]
self._evo_device.set_mode(mode, until=until) # type: ignore[arg-type]
)
@property
def hvac_mode(self) -> HVACMode:
"""Return the current operating mode of a Controller."""
tcs_mode = self._evo_tcs.system_mode
return HVACMode.OFF if tcs_mode == EVO_HEATOFF else HVACMode.HEAT
evo_mode = self._evo_device.system_mode
return HVACMode.OFF if evo_mode in (EVO_HEATOFF, "Off") else HVACMode.HEAT
@property
def current_temperature(self) -> float | None:
@ -418,7 +412,7 @@ class EvoController(EvoClimateEntity):
"""
temps = [
z.temperature
for z in self._evo_tcs.zones.values()
for z in self._evo_device.zones.values()
if z.temperature is not None
]
return round(sum(temps) / len(temps), 1) if temps else None
@ -426,9 +420,9 @@ class EvoController(EvoClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode, e.g., home, away, temp."""
if not self._evo_tcs.system_mode:
if not self._evo_device.system_mode:
return None
return TCS_PRESET_TO_HA.get(self._evo_tcs.system_mode)
return TCS_PRESET_TO_HA.get(self._evo_device.system_mode)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Raise exception as Controllers don't have a target temperature."""
@ -436,9 +430,13 @@ class EvoController(EvoClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set an operating mode for a Controller."""
if not (tcs_mode := HA_HVAC_TO_TCS.get(hvac_mode)):
if hvac_mode == HVACMode.HEAT:
evo_mode = EVO_AUTO if EVO_AUTO in self._evo_modes else "Heat"
elif hvac_mode == HVACMode.OFF:
evo_mode = EVO_HEATOFF if EVO_HEATOFF in self._evo_modes else "Off"
else:
raise HomeAssistantError(f"Invalid hvac_mode: {hvac_mode}")
await self._set_tcs_mode(tcs_mode)
await self._set_tcs_mode(evo_mode)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode; if None, then revert to 'Auto' mode."""
@ -451,6 +449,6 @@ class EvoController(EvoClimateEntity):
attrs = self._device_state_attrs
for attr in STATE_ATTRS_TCS:
if attr == SZ_ACTIVE_FAULTS:
attrs["activeSystemFaults"] = getattr(self._evo_tcs, attr)
attrs["activeSystemFaults"] = getattr(self._evo_device, attr)
else:
attrs[attr] = getattr(self._evo_tcs, attr)
attrs[attr] = getattr(self._evo_device, attr)

View File

@ -42,7 +42,6 @@ class EvoDevice(Entity):
"""Initialize an evohome-compatible entity (TCS, DHW, zone)."""
self._evo_device = evo_device
self._evo_broker = evo_broker
self._evo_tcs = evo_broker.tcs
self._device_state_attrs: dict[str, Any] = {}
@ -101,6 +100,8 @@ class EvoChild(EvoDevice):
"""Initialize an evohome-compatible child entity (DHW, zone)."""
super().__init__(evo_broker, evo_device)
self._evo_tcs = evo_device.tcs
self._schedule: dict[str, Any] = {}
self._setpoints: dict[str, Any] = {}

View File

@ -11,6 +11,7 @@ from unittest.mock import MagicMock, patch
from aiohttp import ClientSession
from evohomeasync2 import EvohomeClient
from evohomeasync2.broker import Broker
from evohomeasync2.controlsystem import ControlSystem
from evohomeasync2.zone import Zone
import pytest
@ -177,13 +178,28 @@ async def evohome(
yield mock_client
@pytest.fixture
async def ctl_id(
hass: HomeAssistant,
config: dict[str, str],
install: MagicMock,
) -> AsyncGenerator[str]:
"""Return the entity_id of the evohome integration's controller."""
async for mock_client in setup_evohome(hass, config, install=install):
evo: EvohomeClient = mock_client.return_value
ctl: ControlSystem = evo._get_single_tcs()
yield f"{Platform.CLIMATE}.{slugify(ctl.location.name)}"
@pytest.fixture
async def zone_id(
hass: HomeAssistant,
config: dict[str, str],
install: MagicMock,
) -> AsyncGenerator[str]:
"""Return the entity_id of the evohome integration' first Climate zone."""
"""Return the entity_id of the evohome integration's first zone."""
async for mock_client in setup_evohome(hass, config, install=install):
evo: EvohomeClient = mock_client.return_value

View File

@ -1,4 +1,124 @@
# serializer version: 1
# name: test_ctl_set_hvac_mode[default]
list([
tuple(
'HeatingOff',
),
tuple(
'Auto',
),
])
# ---
# name: test_ctl_set_hvac_mode[h032585]
list([
tuple(
'Off',
),
tuple(
'Heat',
),
])
# ---
# name: test_ctl_set_hvac_mode[h099625]
list([
tuple(
'HeatingOff',
),
tuple(
'Auto',
),
])
# ---
# name: test_ctl_set_hvac_mode[minimal]
list([
tuple(
'HeatingOff',
),
tuple(
'Auto',
),
])
# ---
# name: test_ctl_set_hvac_mode[sys_004]
list([
tuple(
'HeatingOff',
),
tuple(
'Auto',
),
])
# ---
# name: test_ctl_turn_off[default]
list([
tuple(
'HeatingOff',
),
])
# ---
# name: test_ctl_turn_off[h032585]
list([
tuple(
'Off',
),
])
# ---
# name: test_ctl_turn_off[h099625]
list([
tuple(
'HeatingOff',
),
])
# ---
# name: test_ctl_turn_off[minimal]
list([
tuple(
'HeatingOff',
),
])
# ---
# name: test_ctl_turn_off[sys_004]
list([
tuple(
'HeatingOff',
),
])
# ---
# name: test_ctl_turn_on[default]
list([
tuple(
'Auto',
),
])
# ---
# name: test_ctl_turn_on[h032585]
list([
tuple(
'Heat',
),
])
# ---
# name: test_ctl_turn_on[h099625]
list([
tuple(
'Auto',
),
])
# ---
# name: test_ctl_turn_on[minimal]
list([
tuple(
'Auto',
),
])
# ---
# name: test_ctl_turn_on[sys_004]
list([
tuple(
'Auto',
),
])
# ---
# name: test_setup_platform[botched][climate.bathroom_dn-state]
StateSnapshot({
'attributes': ReadOnlyDict({

View File

@ -27,6 +27,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .conftest import setup_evohome
from .const import TEST_INSTALLS
@ -53,13 +54,142 @@ async def test_setup_platform(
assert x == snapshot(name=f"{x.entity_id}-state")
@pytest.mark.parametrize("install", TEST_INSTALLS)
async def test_ctl_set_hvac_mode(
hass: HomeAssistant,
ctl_id: str,
snapshot: SnapshotAssertion,
) -> None:
"""Test SERVICE_SET_HVAC_MODE of an evohome controller."""
results = []
# SERVICE_SET_HVAC_MODE: HVACMode.OFF
with patch("evohomeasync2.controlsystem.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
Platform.CLIMATE,
SERVICE_SET_HVAC_MODE,
{
ATTR_ENTITY_ID: ctl_id,
ATTR_HVAC_MODE: HVACMode.OFF,
},
blocking=True,
)
assert mock_fcn.await_count == 1
assert mock_fcn.await_args.args != () # 'HeatingOff' or 'Off'
assert mock_fcn.await_args.kwargs == {"until": None}
results.append(mock_fcn.await_args.args)
# SERVICE_SET_HVAC_MODE: HVACMode.HEAT
with patch("evohomeasync2.controlsystem.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
Platform.CLIMATE,
SERVICE_SET_HVAC_MODE,
{
ATTR_ENTITY_ID: ctl_id,
ATTR_HVAC_MODE: HVACMode.HEAT,
},
blocking=True,
)
assert mock_fcn.await_count == 1
assert mock_fcn.await_args.args != () # 'Auto' or 'Heat'
assert mock_fcn.await_args.kwargs == {"until": None}
results.append(mock_fcn.await_args.args)
assert results == snapshot
@pytest.mark.parametrize("install", TEST_INSTALLS)
async def test_ctl_set_temperature(
hass: HomeAssistant,
ctl_id: str,
) -> None:
"""Test SERVICE_SET_TEMPERATURE of an evohome controller."""
# Entity climate.xxx does not support this service
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
Platform.CLIMATE,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: ctl_id,
ATTR_TEMPERATURE: 19.1,
},
blocking=True,
)
@pytest.mark.parametrize("install", TEST_INSTALLS)
async def test_ctl_turn_off(
hass: HomeAssistant,
ctl_id: str,
snapshot: SnapshotAssertion,
) -> None:
"""Test SERVICE_TURN_OFF of an evohome controller."""
results = []
# SERVICE_TURN_OFF
with patch("evohomeasync2.controlsystem.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
Platform.CLIMATE,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: ctl_id,
},
blocking=True,
)
assert mock_fcn.await_count == 1
assert mock_fcn.await_args.args != () # 'HeatingOff' or 'Off'
assert mock_fcn.await_args.kwargs == {"until": None}
results.append(mock_fcn.await_args.args)
assert results == snapshot
@pytest.mark.parametrize("install", TEST_INSTALLS)
async def test_ctl_turn_on(
hass: HomeAssistant,
ctl_id: str,
snapshot: SnapshotAssertion,
) -> None:
"""Test SERVICE_TURN_ON of an evohome controller."""
results = []
# SERVICE_TURN_ON
with patch("evohomeasync2.controlsystem.ControlSystem.set_mode") as mock_fcn:
await hass.services.async_call(
Platform.CLIMATE,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: ctl_id,
},
blocking=True,
)
assert mock_fcn.await_count == 1
assert mock_fcn.await_args.args != () # 'Auto' or 'Heat'
assert mock_fcn.await_args.kwargs == {"until": None}
results.append(mock_fcn.await_args.args)
assert results == snapshot
@pytest.mark.parametrize("install", TEST_INSTALLS)
async def test_zone_set_hvac_mode(
hass: HomeAssistant,
zone_id: str,
snapshot: SnapshotAssertion,
) -> None:
"""Test SERVICE_SET_HVAC_MODE of an evohome zone Climate entity."""
"""Test SERVICE_SET_HVAC_MODE of an evohome heating zone."""
results = []
@ -107,7 +237,7 @@ async def test_zone_set_preset_mode(
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
) -> None:
"""Test SERVICE_SET_PRESET_MODE of an evohome zone Climate entity."""
"""Test SERVICE_SET_PRESET_MODE of an evohome heating zone."""
freezer.move_to("2024-07-10T12:00:00Z")
results = []
@ -175,7 +305,7 @@ async def test_zone_set_temperature(
freezer: FrozenDateTimeFactory,
snapshot: SnapshotAssertion,
) -> None:
"""Test SERVICE_SET_TEMPERATURE of an evohome zone Climate entity."""
"""Test SERVICE_SET_TEMPERATURE of an evohome heating zone."""
freezer.move_to("2024-07-10T12:00:00Z")
results = []
@ -207,7 +337,7 @@ async def test_zone_turn_off(
zone_id: str,
snapshot: SnapshotAssertion,
) -> None:
"""Test SERVICE_TURN_OFF of a evohome zone Climate entity."""
"""Test SERVICE_TURN_OFF of an evohome heating zone."""
results = []
@ -236,7 +366,7 @@ async def test_zone_turn_on(
hass: HomeAssistant,
zone_id: str,
) -> None:
"""Test SERVICE_TURN_ON of a evohome zone Climate entity."""
"""Test SERVICE_TURN_ON of an evohome heating zone."""
# SERVICE_TURN_ON
with patch("evohomeasync2.zone.Zone.reset_mode") as mock_fcn: