mirror of
https://github.com/Electric-Special/ha-core.git
synced 2026-03-21 03:03:17 +01:00
Add zone temperature support to Daikin integration (#152642)
This commit is contained in:
@@ -2,9 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pydaikin.daikin_base import Appliance
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_HVAC_MODE,
|
||||
@@ -21,6 +24,7 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
@@ -29,12 +33,19 @@ from .const import (
|
||||
ATTR_STATE_OFF,
|
||||
ATTR_STATE_ON,
|
||||
ATTR_TARGET_TEMPERATURE,
|
||||
DOMAIN,
|
||||
ZONE_NAME_UNCONFIGURED,
|
||||
)
|
||||
from .coordinator import DaikinConfigEntry, DaikinCoordinator
|
||||
from .entity import DaikinEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type DaikinZone = Sequence[str | int]
|
||||
|
||||
DAIKIN_ZONE_TEMP_HEAT = "lztemp_h"
|
||||
DAIKIN_ZONE_TEMP_COOL = "lztemp_c"
|
||||
|
||||
|
||||
HA_STATE_TO_DAIKIN = {
|
||||
HVACMode.FAN_ONLY: "fan",
|
||||
@@ -78,6 +89,70 @@ HA_ATTR_TO_DAIKIN = {
|
||||
}
|
||||
|
||||
DAIKIN_ATTR_ADVANCED = "adv"
|
||||
ZONE_TEMPERATURE_WINDOW = 2
|
||||
|
||||
|
||||
def _zone_error(
|
||||
translation_key: str, placeholders: dict[str, str] | None = None
|
||||
) -> HomeAssistantError:
|
||||
"""Return a Home Assistant error with Daikin translation info."""
|
||||
return HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders=placeholders,
|
||||
)
|
||||
|
||||
|
||||
def _zone_is_configured(zone: DaikinZone) -> bool:
|
||||
"""Return True if the Daikin zone represents a configured zone."""
|
||||
if not zone:
|
||||
return False
|
||||
return zone[0] != ZONE_NAME_UNCONFIGURED
|
||||
|
||||
|
||||
def _zone_temperature_lists(device: Appliance) -> tuple[list[str], list[str]]:
|
||||
"""Return the decoded zone temperature lists."""
|
||||
try:
|
||||
heating = device.represent(DAIKIN_ZONE_TEMP_HEAT)[1]
|
||||
cooling = device.represent(DAIKIN_ZONE_TEMP_COOL)[1]
|
||||
except AttributeError:
|
||||
return ([], [])
|
||||
return (list(heating or []), list(cooling or []))
|
||||
|
||||
|
||||
def _supports_zone_temperature_control(device: Appliance) -> bool:
|
||||
"""Return True if the device exposes zone temperature settings."""
|
||||
zones = device.zones
|
||||
if not zones:
|
||||
return False
|
||||
heating, cooling = _zone_temperature_lists(device)
|
||||
return bool(
|
||||
heating
|
||||
and cooling
|
||||
and len(heating) >= len(zones)
|
||||
and len(cooling) >= len(zones)
|
||||
)
|
||||
|
||||
|
||||
def _system_target_temperature(device: Appliance) -> float | None:
|
||||
"""Return the system target temperature when available."""
|
||||
target = device.target_temperature
|
||||
if target is None:
|
||||
return None
|
||||
try:
|
||||
return float(target)
|
||||
except TypeError, ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _zone_temperature_from_list(values: list[str], zone_id: int) -> float | None:
|
||||
"""Return the parsed temperature for a zone from a Daikin list."""
|
||||
if zone_id >= len(values):
|
||||
return None
|
||||
try:
|
||||
return float(values[zone_id])
|
||||
except TypeError, ValueError:
|
||||
return None
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -86,8 +161,16 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Daikin climate based on config_entry."""
|
||||
daikin_api = entry.runtime_data
|
||||
async_add_entities([DaikinClimate(daikin_api)])
|
||||
coordinator = entry.runtime_data
|
||||
entities: list[ClimateEntity] = [DaikinClimate(coordinator)]
|
||||
if _supports_zone_temperature_control(coordinator.device):
|
||||
zones = coordinator.device.zones or []
|
||||
entities.extend(
|
||||
DaikinZoneClimate(coordinator, zone_id)
|
||||
for zone_id, zone in enumerate(zones)
|
||||
if _zone_is_configured(zone)
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
def format_target_temperature(target_temperature: float) -> str:
|
||||
@@ -284,3 +367,130 @@ class DaikinClimate(DaikinEntity, ClimateEntity):
|
||||
{HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE]: HA_STATE_TO_DAIKIN[HVACMode.OFF]}
|
||||
)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
|
||||
class DaikinZoneClimate(DaikinEntity, ClimateEntity):
|
||||
"""Representation of a Daikin zone temperature controller."""
|
||||
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_has_entity_name = True
|
||||
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
_attr_target_temperature_step = 1
|
||||
|
||||
def __init__(self, coordinator: DaikinCoordinator, zone_id: int) -> None:
|
||||
"""Initialize the zone climate entity."""
|
||||
super().__init__(coordinator)
|
||||
self._zone_id = zone_id
|
||||
self._attr_unique_id = f"{self.device.mac}-zone{zone_id}-temperature"
|
||||
zone_name = self.device.zones[self._zone_id][0]
|
||||
self._attr_name = f"{zone_name} temperature"
|
||||
|
||||
@property
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Return the hvac modes (mirrors the main unit)."""
|
||||
return [self.hvac_mode]
|
||||
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode:
|
||||
"""Return the current HVAC mode."""
|
||||
daikin_mode = self.device.represent(HA_ATTR_TO_DAIKIN[ATTR_HVAC_MODE])[1]
|
||||
return DAIKIN_TO_HA_STATE.get(daikin_mode, HVACMode.HEAT_COOL)
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
"""Return the current HVAC action."""
|
||||
return HA_STATE_TO_CURRENT_HVAC.get(self.hvac_mode)
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the zone target temperature for the active mode."""
|
||||
heating, cooling = _zone_temperature_lists(self.device)
|
||||
mode = self.hvac_mode
|
||||
if mode == HVACMode.HEAT:
|
||||
return _zone_temperature_from_list(heating, self._zone_id)
|
||||
if mode == HVACMode.COOL:
|
||||
return _zone_temperature_from_list(cooling, self._zone_id)
|
||||
return None
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum selectable temperature."""
|
||||
target = _system_target_temperature(self.device)
|
||||
if target is None:
|
||||
return super().min_temp
|
||||
return target - ZONE_TEMPERATURE_WINDOW
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum selectable temperature."""
|
||||
target = _system_target_temperature(self.device)
|
||||
if target is None:
|
||||
return super().max_temp
|
||||
return target + ZONE_TEMPERATURE_WINDOW
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the entity is available."""
|
||||
return (
|
||||
super().available
|
||||
and _supports_zone_temperature_control(self.device)
|
||||
and _system_target_temperature(self.device) is not None
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return additional metadata."""
|
||||
return {"zone_id": self._zone_id}
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the zone temperature."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="zone_temperature_missing",
|
||||
)
|
||||
zones = self.device.zones
|
||||
if not zones or not _supports_zone_temperature_control(self.device):
|
||||
raise _zone_error("zone_parameters_unavailable")
|
||||
|
||||
try:
|
||||
zone = zones[self._zone_id]
|
||||
except (IndexError, TypeError) as err:
|
||||
raise _zone_error(
|
||||
"zone_missing",
|
||||
{
|
||||
"zone_id": str(self._zone_id),
|
||||
"max_zone": str(len(zones) - 1),
|
||||
},
|
||||
) from err
|
||||
|
||||
if not _zone_is_configured(zone):
|
||||
raise _zone_error("zone_inactive", {"zone_id": str(self._zone_id)})
|
||||
|
||||
temperature_value = float(temperature)
|
||||
target = _system_target_temperature(self.device)
|
||||
if target is None:
|
||||
raise _zone_error("zone_parameters_unavailable")
|
||||
|
||||
mode = self.hvac_mode
|
||||
if mode == HVACMode.HEAT:
|
||||
zone_key = DAIKIN_ZONE_TEMP_HEAT
|
||||
elif mode == HVACMode.COOL:
|
||||
zone_key = DAIKIN_ZONE_TEMP_COOL
|
||||
else:
|
||||
raise _zone_error("zone_hvac_mode_unsupported")
|
||||
|
||||
zone_value = str(round(temperature_value))
|
||||
try:
|
||||
await self.device.set_zone(self._zone_id, zone_key, zone_value)
|
||||
except (AttributeError, KeyError, NotImplementedError, TypeError) as err:
|
||||
raise _zone_error("zone_set_failed") from err
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Disallow changing HVAC mode via zone climate."""
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="zone_hvac_read_only",
|
||||
)
|
||||
|
||||
@@ -24,4 +24,6 @@ ATTR_STATE_OFF = "off"
|
||||
KEY_MAC = "mac"
|
||||
KEY_IP = "ip"
|
||||
|
||||
ZONE_NAME_UNCONFIGURED = "-"
|
||||
|
||||
TIMEOUT_SEC = 120
|
||||
|
||||
@@ -57,5 +57,28 @@
|
||||
"name": "Power"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"zone_hvac_mode_unsupported": {
|
||||
"message": "Zone temperature can only be changed when the main climate mode is heat or cool."
|
||||
},
|
||||
"zone_hvac_read_only": {
|
||||
"message": "Zone HVAC mode is controlled by the main climate entity."
|
||||
},
|
||||
"zone_inactive": {
|
||||
"message": "Zone {zone_id} is not active. Enable the zone on your Daikin device first."
|
||||
},
|
||||
"zone_missing": {
|
||||
"message": "Zone {zone_id} does not exist. Available zones are 0-{max_zone}."
|
||||
},
|
||||
"zone_parameters_unavailable": {
|
||||
"message": "This device does not expose the required zone temperature parameters."
|
||||
},
|
||||
"zone_set_failed": {
|
||||
"message": "Failed to set zone temperature. The device may not support this operation."
|
||||
},
|
||||
"zone_temperature_missing": {
|
||||
"message": "Provide a temperature value when adjusting a zone."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import ZONE_NAME_UNCONFIGURED
|
||||
from .coordinator import DaikinConfigEntry, DaikinCoordinator
|
||||
from .entity import DaikinEntity
|
||||
|
||||
@@ -28,7 +29,7 @@ async def async_setup_entry(
|
||||
switches.extend(
|
||||
DaikinZoneSwitch(daikin_api, zone_id)
|
||||
for zone_id, zone in enumerate(zones)
|
||||
if zone[0] != "-"
|
||||
if zone[0] != ZONE_NAME_UNCONFIGURED
|
||||
)
|
||||
if daikin_api.device.support_advanced_modes:
|
||||
# It isn't possible to find out from the API responses if a specific
|
||||
|
||||
109
tests/components/daikin/conftest.py
Normal file
109
tests/components/daikin/conftest.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""Fixtures for Daikin tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
import re
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import urllib.parse
|
||||
|
||||
import pytest
|
||||
|
||||
type ZoneDefinition = list[str | int]
|
||||
type ZoneDevice = MagicMock
|
||||
|
||||
|
||||
def _decode_zone_values(value: str) -> list[str]:
|
||||
"""Decode a semicolon separated list into zone values."""
|
||||
return re.findall(r"[^;]+", urllib.parse.unquote(value))
|
||||
|
||||
|
||||
def configure_zone_device(
|
||||
zone_device: ZoneDevice,
|
||||
*,
|
||||
zones: list[ZoneDefinition],
|
||||
target_temperature: float | None = 22,
|
||||
mode: str = "hot",
|
||||
heating_values: str | None = None,
|
||||
cooling_values: str | None = None,
|
||||
) -> None:
|
||||
"""Configure a mocked zone-capable Daikin device for a test."""
|
||||
zone_device.target_temperature = target_temperature
|
||||
zone_device.zones = zones
|
||||
zone_device._mode = mode
|
||||
|
||||
encoded_zone_temperatures = ";".join(str(zone[2]) for zone in zones)
|
||||
zone_device.values = {
|
||||
"name": "Daikin Test",
|
||||
"model": "TESTMODEL",
|
||||
"ver": "1_0_0",
|
||||
"zone_name": ";".join(str(zone[0]) for zone in zones),
|
||||
"zone_onoff": ";".join(str(zone[1]) for zone in zones),
|
||||
"lztemp_h": (
|
||||
encoded_zone_temperatures if heating_values is None else heating_values
|
||||
),
|
||||
"lztemp_c": (
|
||||
encoded_zone_temperatures if cooling_values is None else cooling_values
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def zone_device() -> Generator[ZoneDevice]:
|
||||
"""Return a mocked zone-capable Daikin device and patch its factory."""
|
||||
device = MagicMock(name="DaikinZoneDevice")
|
||||
device.mac = "001122334455"
|
||||
device.fan_rate = []
|
||||
device.swing_modes = []
|
||||
device.support_away_mode = False
|
||||
device.support_advanced_modes = False
|
||||
device.support_fan_rate = False
|
||||
device.support_swing_mode = False
|
||||
device.support_outside_temperature = False
|
||||
device.support_energy_consumption = False
|
||||
device.support_humidity = False
|
||||
device.support_compressor_frequency = False
|
||||
device.compressor_frequency = 0
|
||||
device.inside_temperature = 21.0
|
||||
device.outside_temperature = 13.0
|
||||
device.humidity = 40
|
||||
device.current_total_power_consumption = 0.0
|
||||
device.last_hour_cool_energy_consumption = 0.0
|
||||
device.last_hour_heat_energy_consumption = 0.0
|
||||
device.today_energy_consumption = 0.0
|
||||
device.today_total_energy_consumption = 0.0
|
||||
|
||||
configure_zone_device(device, zones=[["Living", "1", 22]])
|
||||
|
||||
def _represent(key: str) -> tuple[None, list[str] | str]:
|
||||
dynamic_values: dict[str, Callable[[], list[str] | str]] = {
|
||||
"lztemp_h": lambda: _decode_zone_values(device.values["lztemp_h"]),
|
||||
"lztemp_c": lambda: _decode_zone_values(device.values["lztemp_c"]),
|
||||
"mode": lambda: device._mode,
|
||||
"f_rate": lambda: "auto",
|
||||
"f_dir": lambda: "3d",
|
||||
"en_hol": lambda: "off",
|
||||
"adv": lambda: "",
|
||||
"htemp": lambda: str(device.inside_temperature),
|
||||
"otemp": lambda: str(device.outside_temperature),
|
||||
}
|
||||
return (None, dynamic_values.get(key, lambda: "")())
|
||||
|
||||
async def _set(values: dict[str, Any]) -> None:
|
||||
if mode := values.get("mode"):
|
||||
device._mode = mode
|
||||
|
||||
device.represent = MagicMock(side_effect=_represent)
|
||||
device.update_status = AsyncMock()
|
||||
device.set = AsyncMock(side_effect=_set)
|
||||
device.set_zone = AsyncMock()
|
||||
device.set_holiday = AsyncMock()
|
||||
device.set_advanced_mode = AsyncMock()
|
||||
device.set_streamer = AsyncMock()
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.daikin.DaikinFactory",
|
||||
new=AsyncMock(return_value=device),
|
||||
):
|
||||
yield device
|
||||
353
tests/components/daikin/test_zone_climate.py
Normal file
353
tests/components/daikin/test_zone_climate.py
Normal file
@@ -0,0 +1,353 @@
|
||||
"""Tests for Daikin zone climate entities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_ACTION,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_HVAC_MODES,
|
||||
ATTR_MAX_TEMP,
|
||||
ATTR_MIN_TEMP,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.components.daikin.const import DOMAIN, KEY_MAC
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_TEMPERATURE,
|
||||
CONF_HOST,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
|
||||
from .conftest import ZoneDevice, configure_zone_device
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
|
||||
|
||||
async def _async_setup_daikin(
|
||||
hass: HomeAssistant, zone_device: ZoneDevice
|
||||
) -> MockConfigEntry:
|
||||
"""Set up a Daikin config entry with a mocked library device."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id=zone_device.mac,
|
||||
data={CONF_HOST: HOST, KEY_MAC: zone_device.mac},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
|
||||
|
||||
def _zone_entity_id(
|
||||
entity_registry: er.EntityRegistry, zone_device: ZoneDevice, zone_id: int
|
||||
) -> str | None:
|
||||
"""Return the entity id for a zone climate unique id."""
|
||||
return entity_registry.async_get_entity_id(
|
||||
CLIMATE_DOMAIN,
|
||||
DOMAIN,
|
||||
f"{zone_device.mac}-zone{zone_id}-temperature",
|
||||
)
|
||||
|
||||
|
||||
async def _async_set_zone_temperature(
|
||||
hass: HomeAssistant, entity_id: str, temperature: float
|
||||
) -> None:
|
||||
"""Call `climate.set_temperature` for a zone climate."""
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_TEMPERATURE: temperature,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
async def test_setup_entry_adds_zone_climates(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Configured zones create zone climate entities."""
|
||||
configure_zone_device(
|
||||
zone_device, zones=[["-", "0", 0], ["Living", "1", 22], ["Office", "1", 21]]
|
||||
)
|
||||
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
|
||||
assert _zone_entity_id(entity_registry, zone_device, 0) is None
|
||||
assert _zone_entity_id(entity_registry, zone_device, 1) is not None
|
||||
assert _zone_entity_id(entity_registry, zone_device, 2) is not None
|
||||
|
||||
|
||||
async def test_setup_entry_skips_zone_climates_without_support(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Missing zone temperature lists skip zone climate entities."""
|
||||
configure_zone_device(zone_device, zones=[["Living", "1", 22]])
|
||||
zone_device.values["lztemp_h"] = ""
|
||||
zone_device.values["lztemp_c"] = ""
|
||||
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
|
||||
assert _zone_entity_id(entity_registry, zone_device, 0) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "expected_zone_key"),
|
||||
[("hot", "lztemp_h"), ("cool", "lztemp_c")],
|
||||
)
|
||||
async def test_zone_climate_sets_temperature_for_active_mode(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
mode: str,
|
||||
expected_zone_key: str,
|
||||
) -> None:
|
||||
"""Setting temperature updates the active mode zone value."""
|
||||
configure_zone_device(
|
||||
zone_device,
|
||||
zones=[["Living", "1", 22], ["Office", "1", 21]],
|
||||
mode=mode,
|
||||
)
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
|
||||
await _async_set_zone_temperature(hass, entity_id, 23)
|
||||
|
||||
zone_device.set_zone.assert_awaited_once_with(0, expected_zone_key, "23")
|
||||
|
||||
|
||||
async def test_zone_climate_rejects_out_of_range_temperature(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Service validation rejects values outside the allowed range."""
|
||||
configure_zone_device(
|
||||
zone_device,
|
||||
zones=[["Living", "1", 22]],
|
||||
target_temperature=22,
|
||||
)
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
|
||||
with pytest.raises(ServiceValidationError) as err:
|
||||
await _async_set_zone_temperature(hass, entity_id, 30)
|
||||
|
||||
assert err.value.translation_key == "temp_out_of_range"
|
||||
|
||||
|
||||
async def test_zone_climate_unavailable_without_target_temperature(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Zones are unavailable if system target temperature is missing."""
|
||||
configure_zone_device(
|
||||
zone_device,
|
||||
zones=[["Living", "1", 22]],
|
||||
target_temperature=None,
|
||||
)
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_zone_climate_zone_inactive_after_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Inactive zones raise a translated error during service calls."""
|
||||
configure_zone_device(zone_device, zones=[["Living", "1", 22]])
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
zone_device.zones[0][0] = "-"
|
||||
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await _async_set_zone_temperature(hass, entity_id, 21)
|
||||
|
||||
assert err.value.translation_key == "zone_inactive"
|
||||
|
||||
|
||||
async def test_zone_climate_zone_missing_after_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Missing zones raise a translated error during service calls."""
|
||||
configure_zone_device(
|
||||
zone_device,
|
||||
zones=[["Living", "1", 22], ["Office", "1", 22]],
|
||||
)
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 1)
|
||||
assert entity_id is not None
|
||||
zone_device.zones = [["Living", "1", 22]]
|
||||
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await _async_set_zone_temperature(hass, entity_id, 21)
|
||||
|
||||
assert err.value.translation_key == "zone_missing"
|
||||
|
||||
|
||||
async def test_zone_climate_parameters_unavailable(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Missing zone parameter lists make the zone entity unavailable."""
|
||||
configure_zone_device(zone_device, zones=[["Living", "1", 22]])
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
zone_device.values["lztemp_h"] = ""
|
||||
zone_device.values["lztemp_c"] = ""
|
||||
|
||||
await async_update_entity(hass, entity_id)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_zone_climate_hvac_modes_read_only(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Changing HVAC mode through a zone climate is blocked."""
|
||||
configure_zone_device(zone_device, zones=[["Living", "1", 22]])
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{
|
||||
ATTR_ENTITY_ID: entity_id,
|
||||
ATTR_HVAC_MODE: HVACMode.HEAT,
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert err.value.translation_key == "zone_hvac_read_only"
|
||||
|
||||
|
||||
async def test_zone_climate_set_temperature_requires_heat_or_cool(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Setting temperature in unsupported modes raises a translated error."""
|
||||
configure_zone_device(
|
||||
zone_device,
|
||||
zones=[["Living", "1", 22]],
|
||||
mode="auto",
|
||||
)
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await _async_set_zone_temperature(hass, entity_id, 21)
|
||||
|
||||
assert err.value.translation_key == "zone_hvac_mode_unsupported"
|
||||
|
||||
|
||||
async def test_zone_climate_properties(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Zone climate exposes expected state attributes."""
|
||||
configure_zone_device(
|
||||
zone_device,
|
||||
zones=[["Living", "1", 22]],
|
||||
target_temperature=24,
|
||||
mode="cool",
|
||||
heating_values="20",
|
||||
cooling_values="18",
|
||||
)
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.COOL
|
||||
assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING
|
||||
assert state.attributes[ATTR_TEMPERATURE] == 18.0
|
||||
assert state.attributes[ATTR_MIN_TEMP] == 22.0
|
||||
assert state.attributes[ATTR_MAX_TEMP] == 26.0
|
||||
assert state.attributes[ATTR_HVAC_MODES] == [HVACMode.COOL]
|
||||
assert state.attributes["zone_id"] == 0
|
||||
|
||||
|
||||
async def test_zone_climate_target_temperature_inactive_mode(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""In non-heating/cooling modes, zone target temperature is None."""
|
||||
configure_zone_device(
|
||||
zone_device,
|
||||
zones=[["Living", "1", 22]],
|
||||
mode="auto",
|
||||
heating_values="bad",
|
||||
cooling_values="19",
|
||||
)
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == HVACMode.HEAT_COOL
|
||||
assert state.attributes[ATTR_TEMPERATURE] is None
|
||||
|
||||
|
||||
async def test_zone_climate_set_zone_failed(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
zone_device: ZoneDevice,
|
||||
) -> None:
|
||||
"""Service call surfaces backend zone update errors."""
|
||||
configure_zone_device(zone_device, zones=[["Living", "1", 22]])
|
||||
await _async_setup_daikin(hass, zone_device)
|
||||
entity_id = _zone_entity_id(entity_registry, zone_device, 0)
|
||||
assert entity_id is not None
|
||||
zone_device.set_zone = AsyncMock(side_effect=NotImplementedError)
|
||||
|
||||
with pytest.raises(HomeAssistantError) as err:
|
||||
await _async_set_zone_temperature(hass, entity_id, 21)
|
||||
|
||||
assert err.value.translation_key == "zone_set_failed"
|
||||
Reference in New Issue
Block a user