mirror of
https://github.com/home-assistant/core.git
synced 2026-07-13 01:27:57 +01:00
iaqualink: complete test coverage, bump to silver (#168268)
This commit is contained in:
@@ -35,6 +35,7 @@ from homeassistant.util.ssl import SSL_ALPN_HTTP11_HTTP2
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AqualinkDataUpdateCoordinator
|
||||
from .entity import AqualinkEntity
|
||||
from .utils import error_detail
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -86,7 +87,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AqualinkConfigEntry) ->
|
||||
except (AqualinkServiceException, TimeoutError, httpx.HTTPError) as aio_exception:
|
||||
await aqualink.close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while attempting login: {aio_exception}"
|
||||
f"Error while attempting login: {error_detail(aio_exception)}"
|
||||
) from aio_exception
|
||||
|
||||
try:
|
||||
@@ -96,10 +97,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AqualinkConfigEntry) ->
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Invalid credentials for iAquaLink"
|
||||
) from auth_exception
|
||||
except AqualinkServiceException as svc_exception:
|
||||
except (AqualinkServiceException, TimeoutError, httpx.HTTPError) as svc_exception:
|
||||
await aqualink.close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while attempting to retrieve systems list: {svc_exception}"
|
||||
"Error while attempting to retrieve systems list: "
|
||||
f"{error_detail(svc_exception)}"
|
||||
) from svc_exception
|
||||
|
||||
systems_list = list(systems.values())
|
||||
@@ -132,10 +134,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: AqualinkConfigEntry) ->
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Invalid credentials for iAquaLink"
|
||||
) from auth_exception
|
||||
except AqualinkServiceException as svc_exception:
|
||||
except (
|
||||
AqualinkServiceException,
|
||||
TimeoutError,
|
||||
httpx.HTTPError,
|
||||
) as svc_exception:
|
||||
await aqualink.close()
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while attempting to retrieve devices list: {svc_exception}"
|
||||
"Error while attempting to retrieve devices list: "
|
||||
f"{error_detail(svc_exception)}"
|
||||
) from svc_exception
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
@@ -74,9 +74,13 @@ class HassAqualinkThermostat(AqualinkEntity[AqualinkThermostat], ClimateEntity):
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Turn the underlying heater switch on or off."""
|
||||
if hvac_mode == HVACMode.HEAT:
|
||||
await await_or_reraise(self.dev.turn_on())
|
||||
await await_or_reraise(
|
||||
self.hass, self.coordinator.config_entry, self.dev.turn_on()
|
||||
)
|
||||
elif hvac_mode == HVACMode.OFF:
|
||||
await await_or_reraise(self.dev.turn_off())
|
||||
await await_or_reraise(
|
||||
self.hass, self.coordinator.config_entry, self.dev.turn_off()
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning("Unknown operation mode: %s", hvac_mode)
|
||||
|
||||
@@ -98,7 +102,11 @@ class HassAqualinkThermostat(AqualinkEntity[AqualinkThermostat], ClimateEntity):
|
||||
@refresh_system
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
await await_or_reraise(self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE])))
|
||||
await await_or_reraise(
|
||||
self.hass,
|
||||
self.coordinator.config_entry,
|
||||
self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE])),
|
||||
)
|
||||
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Config flow to configure zone component."""
|
||||
"""Config flow for iAquaLink."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
@@ -31,7 +31,7 @@ CREDENTIALS_DATA_SCHEMA = vol.Schema(
|
||||
|
||||
|
||||
class AqualinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Aqualink config flow."""
|
||||
"""iAquaLink config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@@ -50,7 +50,7 @@ class AqualinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
pass
|
||||
except AqualinkServiceUnauthorizedException:
|
||||
return {"base": "invalid_auth"}
|
||||
except AqualinkServiceException, httpx.HTTPError:
|
||||
except AqualinkServiceException, TimeoutError, httpx.HTTPError:
|
||||
return {"base": "cannot_connect"}
|
||||
|
||||
return {}
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, UPDATE_INTERVAL_BY_SYSTEM_TYPE, UPDATE_INTERVAL_DEFAULT
|
||||
from .utils import error_detail
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,9 +52,10 @@ class AqualinkDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
self.system.serial,
|
||||
)
|
||||
return
|
||||
except (AqualinkServiceException, httpx.HTTPError) as err:
|
||||
except (AqualinkServiceException, TimeoutError, httpx.HTTPError) as err:
|
||||
raise UpdateFailed(
|
||||
f"Unable to update iAquaLink system {self.system.serial}: {err}"
|
||||
"Unable to update iAquaLink system "
|
||||
f"{self.system.serial}: {error_detail(err)}"
|
||||
) from err
|
||||
if self.system.online is not True:
|
||||
raise UpdateFailed(f"iAquaLink system {self.system.serial} is offline")
|
||||
|
||||
@@ -67,18 +67,28 @@ class HassAqualinkLight(AqualinkEntity[AqualinkLight], LightEntity):
|
||||
"""
|
||||
# For now I'm assuming lights support either effects or brightness.
|
||||
if effect_name := kwargs.get(ATTR_EFFECT):
|
||||
await await_or_reraise(self.dev.set_effect_by_name(effect_name))
|
||||
await await_or_reraise(
|
||||
self.hass,
|
||||
self.coordinator.config_entry,
|
||||
self.dev.set_effect_by_name(effect_name),
|
||||
)
|
||||
elif brightness := kwargs.get(ATTR_BRIGHTNESS):
|
||||
# Aqualink supports percentages in 25% increments.
|
||||
pct = round(brightness * 4.0 / 255) * 25
|
||||
await await_or_reraise(self.dev.set_brightness(pct))
|
||||
await await_or_reraise(
|
||||
self.hass, self.coordinator.config_entry, self.dev.set_brightness(pct)
|
||||
)
|
||||
else:
|
||||
await await_or_reraise(self.dev.turn_on())
|
||||
await await_or_reraise(
|
||||
self.hass, self.coordinator.config_entry, self.dev.turn_on()
|
||||
)
|
||||
|
||||
@refresh_system
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await await_or_reraise(self.dev.turn_off())
|
||||
await await_or_reraise(
|
||||
self.hass, self.coordinator.config_entry, self.dev.turn_off()
|
||||
)
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
@@ -86,7 +96,7 @@ class HassAqualinkLight(AqualinkEntity[AqualinkLight], LightEntity):
|
||||
|
||||
The scale needs converting between 0-100 and 0-255.
|
||||
"""
|
||||
return self.dev.brightness * 255 / 100
|
||||
return round(self.dev.brightness * 255 / 100)
|
||||
|
||||
@property
|
||||
def effect(self) -> str:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["iaqualink"],
|
||||
"quality_scale": "bronze",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["iaqualink==0.7.0", "h2==4.3.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ rules:
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
@@ -35,7 +35,7 @@ rules:
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
|
||||
@@ -56,9 +56,13 @@ class HassAqualinkSwitch(AqualinkEntity[AqualinkSwitch], SwitchEntity):
|
||||
@refresh_system
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the switch."""
|
||||
await await_or_reraise(self.dev.turn_on())
|
||||
await await_or_reraise(
|
||||
self.hass, self.coordinator.config_entry, self.dev.turn_on()
|
||||
)
|
||||
|
||||
@refresh_system
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the switch."""
|
||||
await await_or_reraise(self.dev.turn_off())
|
||||
await await_or_reraise(
|
||||
self.hass, self.coordinator.config_entry, self.dev.turn_off()
|
||||
)
|
||||
|
||||
@@ -3,14 +3,42 @@
|
||||
from collections.abc import Awaitable
|
||||
|
||||
import httpx
|
||||
from iaqualink.exception import AqualinkServiceException
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
|
||||
|
||||
async def await_or_reraise(awaitable: Awaitable) -> None:
|
||||
def error_detail(err: Exception) -> str:
|
||||
"""Return a non-empty error detail for iaqualink exceptions."""
|
||||
if detail := str(err):
|
||||
return detail
|
||||
return type(err).__name__
|
||||
|
||||
|
||||
async def await_or_reraise(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry | None,
|
||||
awaitable: Awaitable,
|
||||
) -> None:
|
||||
"""Execute API call while catching service exceptions."""
|
||||
try:
|
||||
await awaitable
|
||||
except AqualinkServiceUnauthorizedException as auth_exception:
|
||||
if config_entry is not None:
|
||||
config_entry.async_start_reauth(hass)
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Invalid credentials for iAquaLink"
|
||||
) from auth_exception
|
||||
except TimeoutError as timeout_exception:
|
||||
raise HomeAssistantError(
|
||||
f"Aqualink error: {error_detail(timeout_exception)}"
|
||||
) from timeout_exception
|
||||
except (AqualinkServiceException, httpx.HTTPError) as svc_exception:
|
||||
raise HomeAssistantError(f"Aqualink error: {svc_exception}") from svc_exception
|
||||
raise HomeAssistantError(
|
||||
f"Aqualink error: {error_detail(svc_exception)}"
|
||||
) from svc_exception
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
"""Configuration for iAquaLink tests."""
|
||||
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from unittest.mock import AsyncMock, PropertyMock, patch
|
||||
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.device import AqualinkDevice
|
||||
from iaqualink.system import AqualinkSystem
|
||||
from iaqualink.systems.iaqua.device import (
|
||||
IaquaAuxSwitch,
|
||||
IaquaBinarySensor,
|
||||
IaquaLightSwitch,
|
||||
IaquaSensor,
|
||||
IaquaThermostat,
|
||||
)
|
||||
from iaqualink.systems.iaqua.system import IaquaSystem
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.iaqualink import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -18,14 +29,18 @@ MOCK_PASSWORD = "password"
|
||||
MOCK_DATA = {CONF_USERNAME: MOCK_USERNAME, CONF_PASSWORD: MOCK_PASSWORD}
|
||||
|
||||
|
||||
def async_returns(x):
|
||||
"""Return value-returning async mock."""
|
||||
return AsyncMock(return_value=x)
|
||||
@dataclass
|
||||
class SystemDevices:
|
||||
"""All devices created by setup_integration."""
|
||||
|
||||
|
||||
def async_raises(x):
|
||||
"""Return exception-raising async mock."""
|
||||
return AsyncMock(side_effect=x)
|
||||
system: IaquaSystem
|
||||
switch: IaquaAuxSwitch
|
||||
light: IaquaLightSwitch
|
||||
binary_sensor: IaquaBinarySensor
|
||||
sensor: IaquaSensor
|
||||
thermostat: IaquaThermostat
|
||||
heater: IaquaAuxSwitch
|
||||
pool_temp: IaquaSensor
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
@@ -42,13 +57,129 @@ def get_aqualink_system(aqualink, cls=None, data=None):
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
num = random.randint(0, 99999)
|
||||
data["name"] = "Pool"
|
||||
data["serial_number"] = f"SN{num:05}"
|
||||
data["serial_number"] = "SN00001"
|
||||
|
||||
return cls(aqualink=aqualink, data=data)
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> SystemDevices:
|
||||
"""Set up the iAquaLink integration with all platform entities."""
|
||||
system = get_aqualink_system(
|
||||
client,
|
||||
cls=IaquaSystem,
|
||||
data={"home_screen": [{}, {}, {}, {"temp_scale": "F"}]},
|
||||
)
|
||||
system.online = True
|
||||
|
||||
async def update() -> None:
|
||||
system.temp_unit = "F"
|
||||
|
||||
system.update = AsyncMock(side_effect=update)
|
||||
|
||||
switch = get_aqualink_device(
|
||||
system, name="aux_1", cls=IaquaAuxSwitch, data={"state": "1", "aux": "1"}
|
||||
)
|
||||
light = get_aqualink_device(
|
||||
system,
|
||||
name="aux_2",
|
||||
cls=IaquaLightSwitch,
|
||||
data={"state": "1", "aux": "2", "label": "Pool Light"},
|
||||
)
|
||||
binary_sensor = get_aqualink_device(
|
||||
system, name="freeze_protection", cls=IaquaBinarySensor, data={"state": "1"}
|
||||
)
|
||||
sensor = get_aqualink_device(
|
||||
system, name="ph", cls=IaquaSensor, data={"state": "7.2"}
|
||||
)
|
||||
thermostat = get_aqualink_device(
|
||||
system, name="pool_set_point", cls=IaquaThermostat, data={"state": "84"}
|
||||
)
|
||||
heater = get_aqualink_device(
|
||||
system, name="pool_heater", cls=IaquaAuxSwitch, data={"state": "1", "aux": "3"}
|
||||
)
|
||||
pool_temp = get_aqualink_device(
|
||||
system, name="pool_temp", cls=IaquaSensor, data={"state": "80"}
|
||||
)
|
||||
|
||||
platform_devices = {
|
||||
d.name: d for d in (switch, light, binary_sensor, sensor, thermostat)
|
||||
}
|
||||
system.devices = {
|
||||
**platform_devices,
|
||||
heater.name: heater,
|
||||
pool_temp.name: pool_temp,
|
||||
}
|
||||
system.get_devices = AsyncMock(return_value=platform_devices)
|
||||
system.set_aux = AsyncMock()
|
||||
system.set_temps = AsyncMock()
|
||||
system.set_light = AsyncMock()
|
||||
|
||||
await setup_entry(hass, config_entry, system)
|
||||
|
||||
return SystemDevices(
|
||||
system=system,
|
||||
switch=switch,
|
||||
light=light,
|
||||
binary_sensor=binary_sensor,
|
||||
sensor=sensor,
|
||||
thermostat=thermostat,
|
||||
heater=heater,
|
||||
pool_temp=pool_temp,
|
||||
)
|
||||
|
||||
|
||||
async def setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
system: AqualinkSystem,
|
||||
) -> None:
|
||||
"""Set up a config entry with a pre-built system."""
|
||||
config_entry.add_to_hass(hass)
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
return_value={system.serial: system},
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def assert_platform_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
domain: str,
|
||||
) -> None:
|
||||
"""Assert all entities for a given platform domain are set up correctly."""
|
||||
await setup_integration(hass, config_entry, client)
|
||||
|
||||
entity_entries = [
|
||||
e
|
||||
for e in er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
if e.domain == domain
|
||||
]
|
||||
assert entity_entries
|
||||
for entity_entry in entity_entries:
|
||||
assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")
|
||||
assert hass.states.get(entity_entry.entity_id) == snapshot(
|
||||
name=f"{entity_entry.entity_id}-state"
|
||||
)
|
||||
|
||||
|
||||
def get_aqualink_device(system, name, cls=None, data=None):
|
||||
"""Create aqualink device."""
|
||||
if cls is None:
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[binary_sensor.freeze_protection-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.freeze_protection',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.COLD: 'cold'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_freeze_protection',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[binary_sensor.freeze_protection-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'cold',
|
||||
'friendly_name': 'Freeze Protection',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.freeze_protection',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,68 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[climate.pool_set_point-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 40.0,
|
||||
'min_temp': 1.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.pool_set_point',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_pool_set_point',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[climate.pool_set_point-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 26.7,
|
||||
'friendly_name': 'Pool Set Point',
|
||||
'hvac_action': <HVACAction.HEATING: 'heating'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 40.0,
|
||||
'min_temp': 1.1,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'temperature': 28.9,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.pool_set_point',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,278 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[binary_sensor.freeze_protection-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'binary_sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'binary_sensor.freeze_protection',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <BinarySensorDeviceClass.COLD: 'cold'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_freeze_protection',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[binary_sensor.freeze_protection-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'cold',
|
||||
'friendly_name': 'Freeze Protection',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'binary_sensor.freeze_protection',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[climate.pool_set_point-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 40.0,
|
||||
'min_temp': 1.1,
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'climate',
|
||||
'entity_category': None,
|
||||
'entity_id': 'climate.pool_set_point',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_pool_set_point',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[climate.pool_set_point-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'current_temperature': 26.7,
|
||||
'friendly_name': 'Pool Set Point',
|
||||
'hvac_action': <HVACAction.HEATING: 'heating'>,
|
||||
'hvac_modes': list([
|
||||
<HVACMode.HEAT: 'heat'>,
|
||||
<HVACMode.OFF: 'off'>,
|
||||
]),
|
||||
'max_temp': 40.0,
|
||||
'min_temp': 1.1,
|
||||
'supported_features': <ClimateEntityFeature: 385>,
|
||||
'temperature': 28.9,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'climate.pool_set_point',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'heat',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.pool_light-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.pool_light',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_aux_2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.pool_light-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'color_mode': <ColorMode.ONOFF: 'onoff'>,
|
||||
'friendly_name': 'Pool Light',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.pool_light',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[sensor.ph-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.ph',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_ph',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[sensor.ph-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Ph',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.ph',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '7.2',
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[switch.aux_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.aux_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_aux_1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[switch.aux_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Aux 1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.aux_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,60 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[light.pool_light-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'light',
|
||||
'entity_category': None,
|
||||
'entity_id': 'light.pool_light',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_aux_2',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[light.pool_light-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'color_mode': <ColorMode.ONOFF: 'onoff'>,
|
||||
'friendly_name': 'Pool Light',
|
||||
'supported_color_modes': list([
|
||||
<ColorMode.ONOFF: 'onoff'>,
|
||||
]),
|
||||
'supported_features': <LightEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'light.pool_light',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,51 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[sensor.ph-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'sensor',
|
||||
'entity_category': None,
|
||||
'entity_id': 'sensor.ph',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_ph',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[sensor.ph-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Ph',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'sensor.ph',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '7.2',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,51 @@
|
||||
# serializer version: 1
|
||||
# name: test_setup[switch.aux_1-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': list([
|
||||
None,
|
||||
]),
|
||||
'area_id': None,
|
||||
'capabilities': None,
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'switch',
|
||||
'entity_category': None,
|
||||
'entity_id': 'switch.aux_1',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'object_id_base': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'iaqualink',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': None,
|
||||
'unique_id': 'SN00001_aux_1',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_setup[switch.aux_1-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'Aux 1',
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'switch.aux_1',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Binary sensor platform tests for iAquaLink."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.systems.iaqua.device import IaquaBinarySensor
|
||||
from iaqualink.systems.iaqua.system import IaquaSystem
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import (
|
||||
assert_platform_setup,
|
||||
get_aqualink_device,
|
||||
get_aqualink_system,
|
||||
setup_entry,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test all binary sensor entities are created correctly."""
|
||||
await assert_platform_setup(
|
||||
hass, config_entry, client, entity_registry, snapshot, BINARY_SENSOR_DOMAIN
|
||||
)
|
||||
|
||||
|
||||
async def _setup_binary_sensor(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
*,
|
||||
name: str,
|
||||
state: str,
|
||||
) -> str:
|
||||
"""Set up the integration with a single binary sensor entity."""
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
sensor = get_aqualink_device(
|
||||
system, name=name, cls=IaquaBinarySensor, data={"state": state}
|
||||
)
|
||||
system.get_devices = AsyncMock(return_value={sensor.name: sensor})
|
||||
await setup_entry(hass, config_entry, system)
|
||||
|
||||
entity_ids = hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)
|
||||
assert len(entity_ids) == 1
|
||||
return entity_ids[0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "expected_class"),
|
||||
[
|
||||
pytest.param("freeze_protection", BinarySensorDeviceClass.COLD, id="freeze"),
|
||||
pytest.param("auxiliary", None, id="default"),
|
||||
],
|
||||
)
|
||||
async def test_binary_sensor_device_class(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
entity_registry: er.EntityRegistry,
|
||||
name: str,
|
||||
expected_class: BinarySensorDeviceClass | None,
|
||||
) -> None:
|
||||
"""Test binary sensor device class mapping."""
|
||||
entity_id = await _setup_binary_sensor(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
name=name,
|
||||
state="1",
|
||||
)
|
||||
|
||||
entry = entity_registry.async_get(entity_id)
|
||||
assert entry is not None
|
||||
assert entry.original_device_class == expected_class
|
||||
assert entry.has_entity_name is True
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_binary_sensor_off_state(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test binary sensor off state is exposed through Home Assistant."""
|
||||
entity_id = await _setup_binary_sensor(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
name="freeze_protection",
|
||||
state="0",
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_OFF
|
||||
@@ -0,0 +1,355 @@
|
||||
"""Climate platform tests for iAquaLink."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
from iaqualink.systems.iaqua.device import (
|
||||
AqualinkState,
|
||||
IaquaAuxSwitch,
|
||||
IaquaSensor,
|
||||
IaquaThermostat,
|
||||
)
|
||||
from iaqualink.systems.iaqua.system import IaquaSystem
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_HVAC_MODE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||
|
||||
from .conftest import (
|
||||
assert_platform_setup,
|
||||
get_aqualink_device,
|
||||
get_aqualink_system,
|
||||
setup_entry,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test all climate entities are created correctly."""
|
||||
await assert_platform_setup(
|
||||
hass, config_entry, client, entity_registry, snapshot, CLIMATE_DOMAIN
|
||||
)
|
||||
|
||||
|
||||
async def _setup_thermostat(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
*,
|
||||
temp_unit: str = "F",
|
||||
heater_state: str = AqualinkState.ON.value,
|
||||
target_temperature: str = "84",
|
||||
current_temperature: str = "80",
|
||||
) -> tuple[IaquaSystem, object, object, object, str, object]:
|
||||
"""Set up the integration with a single thermostat entity."""
|
||||
if temp_unit == "F":
|
||||
hass.config.units = US_CUSTOMARY_SYSTEM
|
||||
|
||||
system = get_aqualink_system(
|
||||
client,
|
||||
cls=IaquaSystem,
|
||||
data={"home_screen": [{}, {}, {}, {"temp_scale": temp_unit}]},
|
||||
)
|
||||
system.online = True
|
||||
|
||||
async def update() -> None:
|
||||
system.temp_unit = temp_unit
|
||||
|
||||
system.update = AsyncMock(side_effect=update)
|
||||
heater = get_aqualink_device(
|
||||
system,
|
||||
name="pool_heater",
|
||||
cls=IaquaAuxSwitch,
|
||||
data={"state": heater_state, "aux": "1"},
|
||||
)
|
||||
sensor = get_aqualink_device(
|
||||
system,
|
||||
name="pool_temp",
|
||||
cls=IaquaSensor,
|
||||
data={"state": current_temperature},
|
||||
)
|
||||
thermostat = get_aqualink_device(
|
||||
system,
|
||||
name="pool_set_point",
|
||||
cls=IaquaThermostat,
|
||||
data={"state": target_temperature},
|
||||
)
|
||||
system.devices = {
|
||||
heater.name: heater,
|
||||
sensor.name: sensor,
|
||||
thermostat.name: thermostat,
|
||||
}
|
||||
system.get_devices = AsyncMock(return_value={thermostat.name: thermostat})
|
||||
system.set_aux = AsyncMock()
|
||||
system.set_temps = AsyncMock()
|
||||
|
||||
await setup_entry(hass, config_entry, system)
|
||||
|
||||
entity_ids = hass.states.async_entity_ids(CLIMATE_DOMAIN)
|
||||
assert len(entity_ids) == 1
|
||||
entity_id = entity_ids[0]
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
return system, heater, sensor, thermostat, entity_id, entity_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("heater_state", "expected_action", "expected_mode"),
|
||||
[
|
||||
pytest.param(
|
||||
AqualinkState.ON.value, HVACAction.HEATING, HVACMode.HEAT, id="heating"
|
||||
),
|
||||
pytest.param(
|
||||
AqualinkState.ENABLED.value, HVACAction.IDLE, HVACMode.OFF, id="idle"
|
||||
),
|
||||
pytest.param(AqualinkState.OFF.value, HVACAction.OFF, HVACMode.OFF, id="off"),
|
||||
],
|
||||
)
|
||||
async def test_thermostat_properties(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
heater_state: str,
|
||||
expected_action: HVACAction,
|
||||
expected_mode: HVACMode,
|
||||
) -> None:
|
||||
"""Test thermostat properties and HVAC action mapping."""
|
||||
_, _, _, _, _, entity_state = await _setup_thermostat(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
heater_state=heater_state,
|
||||
)
|
||||
|
||||
assert entity_state.state == expected_mode
|
||||
assert entity_state.attributes["hvac_action"] == expected_action
|
||||
assert entity_state.attributes["temperature"] == 84.0
|
||||
assert entity_state.attributes["current_temperature"] == 80.0
|
||||
|
||||
|
||||
async def test_thermostat_current_temperature_none(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test thermostat current temperature handles empty values."""
|
||||
_, _, _, _, _, entity_state = await _setup_thermostat(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
temp_unit="C",
|
||||
heater_state=AqualinkState.OFF.value,
|
||||
target_temperature="24",
|
||||
current_temperature="",
|
||||
)
|
||||
|
||||
assert entity_state.state == HVACMode.OFF
|
||||
assert entity_state.attributes["current_temperature"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("hvac_mode", "initial_state", "expected_state"),
|
||||
[
|
||||
pytest.param(HVACMode.HEAT, AqualinkState.OFF.value, HVACMode.HEAT, id="heat"),
|
||||
pytest.param(HVACMode.OFF, AqualinkState.ON.value, HVACMode.OFF, id="off"),
|
||||
],
|
||||
)
|
||||
async def test_thermostat_set_hvac_mode(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
hvac_mode: HVACMode,
|
||||
initial_state: str,
|
||||
expected_state: HVACMode,
|
||||
) -> None:
|
||||
"""Test thermostat HVAC mode service updates Home Assistant state."""
|
||||
system, heater, _, _, entity_id, _ = await _setup_thermostat(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
heater_state=initial_state,
|
||||
)
|
||||
|
||||
async def set_aux(_: str) -> None:
|
||||
heater.data["state"] = (
|
||||
AqualinkState.ON.value
|
||||
if hvac_mode == HVACMode.HEAT
|
||||
else AqualinkState.OFF.value
|
||||
)
|
||||
|
||||
system.set_aux = AsyncMock(side_effect=set_aux)
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: hvac_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == expected_state
|
||||
|
||||
|
||||
async def test_thermostat_set_temperature(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test thermostat target temperature service updates Home Assistant state."""
|
||||
system, _, _, thermostat, entity_id, _ = await _setup_thermostat(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
)
|
||||
|
||||
async def set_temps(data: dict[str, str]) -> None:
|
||||
thermostat.data["state"] = data["temp1"]
|
||||
|
||||
system.set_temps = AsyncMock(side_effect=set_temps)
|
||||
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 84.9},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.attributes["temperature"] == 84.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data", "system_attr"),
|
||||
[
|
||||
pytest.param(
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_HVAC_MODE: HVACMode.HEAT},
|
||||
"set_aux",
|
||||
id="set-hvac-mode",
|
||||
),
|
||||
pytest.param(
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_TEMPERATURE: 84},
|
||||
"set_temps",
|
||||
id="set-temperature",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("raised_exception", "expected_exception", "match"),
|
||||
[
|
||||
pytest.param(
|
||||
AqualinkServiceException,
|
||||
HomeAssistantError,
|
||||
"Aqualink error: AqualinkServiceException",
|
||||
id="service",
|
||||
),
|
||||
pytest.param(
|
||||
TimeoutError(),
|
||||
HomeAssistantError,
|
||||
"Aqualink error: TimeoutError",
|
||||
id="timeout",
|
||||
),
|
||||
pytest.param(
|
||||
httpx.HTTPError("boom"),
|
||||
HomeAssistantError,
|
||||
"Aqualink error: boom",
|
||||
id="http",
|
||||
),
|
||||
pytest.param(
|
||||
AqualinkServiceUnauthorizedException,
|
||||
ConfigEntryAuthFailed,
|
||||
"Invalid credentials for iAquaLink",
|
||||
id="unauthorized",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_climate_action_errors_leave_state_unchanged(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
service: str,
|
||||
service_data: dict[str, Any],
|
||||
system_attr: str,
|
||||
raised_exception: Exception | type[Exception],
|
||||
expected_exception: type[Exception],
|
||||
match: str,
|
||||
) -> None:
|
||||
"""Test climate action errors are surfaced through the service call."""
|
||||
system, _, _, _, entity_id, entity_state = await _setup_thermostat(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
heater_state=AqualinkState.OFF.value,
|
||||
)
|
||||
initial_state = entity_state.state
|
||||
setattr(system, system_attr, AsyncMock(side_effect=raised_exception))
|
||||
|
||||
with pytest.raises(expected_exception, match=match):
|
||||
await hass.services.async_call(
|
||||
CLIMATE_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: entity_id, **service_data},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == initial_state
|
||||
|
||||
|
||||
async def test_thermostat_set_unknown_hvac_mode_logs_warning(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test unsupported HVAC modes log a warning and keep state unchanged."""
|
||||
system, _, _, _, entity_id, _ = await _setup_thermostat(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
heater_state=AqualinkState.OFF.value,
|
||||
)
|
||||
climate_component = hass.data[CLIMATE_DOMAIN]
|
||||
entity = climate_component.get_entity(entity_id)
|
||||
assert entity is not None
|
||||
|
||||
with caplog.at_level(logging.WARNING):
|
||||
await entity.async_set_hvac_mode(HVACMode.COOL)
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == HVACMode.OFF
|
||||
assert "Unknown operation mode" in caplog.text
|
||||
system.set_aux.assert_not_called()
|
||||
@@ -1,11 +1,14 @@
|
||||
"""Tests for iAquaLink config flow."""
|
||||
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
|
||||
import httpx
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.iaqualink import DOMAIN, config_flow
|
||||
from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER
|
||||
@@ -23,12 +26,15 @@ DHCP_DISCOVERY = DhcpServiceInfo(
|
||||
)
|
||||
|
||||
|
||||
async def test_already_configured(
|
||||
async def _async_mock_login(self: Any) -> None:
|
||||
"""Mock a successful login."""
|
||||
|
||||
|
||||
async def test_single_instance_allowed(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
config_data: dict[str, str],
|
||||
) -> None:
|
||||
"""Test config flow when iaqualink component is already setup."""
|
||||
"""Test config flow aborts when an entry already exists."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -70,7 +76,7 @@ async def test_dhcp_discovery_starts_user_flow(
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
|
||||
return_value=None,
|
||||
_async_mock_login,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.async_setup_entry",
|
||||
@@ -107,16 +113,26 @@ async def test_with_invalid_credentials(
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
async def test_service_exception(
|
||||
hass: HomeAssistant, config_data: dict[str, str]
|
||||
@pytest.mark.parametrize(
|
||||
"raised_exception",
|
||||
[
|
||||
pytest.param(AqualinkServiceException, id="service"),
|
||||
pytest.param(TimeoutError, id="timeout"),
|
||||
pytest.param(httpx.HTTPError("boom"), id="http"),
|
||||
],
|
||||
)
|
||||
async def test_cannot_connect_exception(
|
||||
hass: HomeAssistant,
|
||||
config_data: dict[str, str],
|
||||
raised_exception: Exception | type[Exception],
|
||||
) -> None:
|
||||
"""Test config flow encountering service exception."""
|
||||
"""Test config flow encountering a connection-related exception."""
|
||||
flow = config_flow.AqualinkFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
|
||||
side_effect=AqualinkServiceException,
|
||||
side_effect=raised_exception,
|
||||
):
|
||||
result = await flow.async_step_user(config_data)
|
||||
|
||||
@@ -131,10 +147,11 @@ async def test_with_existing_config(
|
||||
"""Test config flow with existing configuration."""
|
||||
flow = config_flow.AqualinkFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
|
||||
return_value=None,
|
||||
_async_mock_login,
|
||||
):
|
||||
result = await flow.async_step_user(config_data)
|
||||
|
||||
@@ -169,8 +186,6 @@ async def test_reauth_success(hass: HomeAssistant, config_data: dict[str, str])
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
new_username = "updated@example.com"
|
||||
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -180,7 +195,7 @@ async def test_reauth_success(hass: HomeAssistant, config_data: dict[str, str])
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
|
||||
return_value=None,
|
||||
_async_mock_login,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.config_entries.ConfigEntries.async_reload",
|
||||
@@ -189,16 +204,18 @@ async def test_reauth_success(hass: HomeAssistant, config_data: dict[str, str])
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: new_username, CONF_PASSWORD: "new_password"},
|
||||
{
|
||||
CONF_USERNAME: config_data[CONF_USERNAME],
|
||||
CONF_PASSWORD: "new_password",
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert entry.title == new_username
|
||||
assert entry.title == config_data[CONF_USERNAME]
|
||||
assert dict(entry.data) == {
|
||||
**config_data,
|
||||
CONF_USERNAME: new_username,
|
||||
CONF_PASSWORD: "new_password",
|
||||
}
|
||||
|
||||
@@ -214,8 +231,6 @@ async def test_reconfigure_success(
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
new_username = "updated@example.com"
|
||||
|
||||
result = await entry.start_reconfigure_flow(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
@@ -225,7 +240,7 @@ async def test_reconfigure_success(
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
|
||||
return_value=None,
|
||||
_async_mock_login,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.config_entries.ConfigEntries.async_reload",
|
||||
@@ -234,16 +249,15 @@ async def test_reconfigure_success(
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_USERNAME: new_username, CONF_PASSWORD: "new_password"},
|
||||
{CONF_USERNAME: config_data[CONF_USERNAME], CONF_PASSWORD: "new_password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert entry.title == new_username
|
||||
assert entry.title == config_data[CONF_USERNAME]
|
||||
assert dict(entry.data) == {
|
||||
**config_data,
|
||||
CONF_USERNAME: new_username,
|
||||
CONF_PASSWORD: "new_password",
|
||||
}
|
||||
|
||||
@@ -252,7 +266,10 @@ async def test_reauth_invalid_auth(
|
||||
hass: HomeAssistant, config_data: dict[str, str]
|
||||
) -> None:
|
||||
"""Test reauthentication with invalid credentials."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=config_data)
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=config_data,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
@@ -271,18 +288,31 @@ async def test_reauth_invalid_auth(
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raised_exception",
|
||||
[
|
||||
pytest.param(AqualinkServiceException, id="service"),
|
||||
pytest.param(TimeoutError, id="timeout"),
|
||||
pytest.param(httpx.HTTPError("boom"), id="http"),
|
||||
],
|
||||
)
|
||||
async def test_reauth_cannot_connect(
|
||||
hass: HomeAssistant, config_data: dict[str, str]
|
||||
hass: HomeAssistant,
|
||||
config_data: dict[str, str],
|
||||
raised_exception: Exception | type[Exception],
|
||||
) -> None:
|
||||
"""Test reauthentication when the service cannot be reached."""
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=config_data)
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=config_data,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
|
||||
side_effect=AqualinkServiceException,
|
||||
side_effect=raised_exception,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Entity base tests for iAquaLink."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from iaqualink.client import AqualinkClient
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test all entities are created by setup_integration."""
|
||||
await setup_integration(hass, config_entry, client)
|
||||
|
||||
entity_entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
assert entity_entries
|
||||
for entity_entry in entity_entries:
|
||||
assert entity_entry == snapshot(name=f"{entity_entry.entity_id}-entry")
|
||||
state = hass.states.get(entity_entry.entity_id)
|
||||
assert state == snapshot(name=f"{entity_entry.entity_id}-state")
|
||||
@@ -3,19 +3,14 @@
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import httpx
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceThrottledException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
from iaqualink.systems.iaqua.device import (
|
||||
IaquaAuxSwitch,
|
||||
IaquaBinarySensor,
|
||||
IaquaLightSwitch,
|
||||
IaquaSensor,
|
||||
IaquaThermostat,
|
||||
)
|
||||
from iaqualink.systems.iaqua.device import IaquaLightSwitch
|
||||
from iaqualink.systems.iaqua.system import IaquaSystem
|
||||
import pytest
|
||||
|
||||
@@ -39,7 +34,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import get_aqualink_device, get_aqualink_system
|
||||
from .conftest import get_aqualink_device, get_aqualink_system, setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
@@ -55,37 +50,23 @@ async def _advance_coordinator_time(
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raised_exception",
|
||||
[
|
||||
pytest.param(AqualinkServiceException, id="service"),
|
||||
pytest.param(TimeoutError, id="timeout"),
|
||||
pytest.param(httpx.HTTPError("boom"), id="http"),
|
||||
],
|
||||
)
|
||||
async def test_system_refresh_failure_marks_entities_unavailable(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
raised_exception: Exception | type[Exception],
|
||||
) -> None:
|
||||
"""Test a system refresh failure marks attached entities unavailable."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
systems = {system.serial: system}
|
||||
light = get_aqualink_device(
|
||||
system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
|
||||
)
|
||||
devices = {light.name: light}
|
||||
system.get_devices = AsyncMock(return_value=devices)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
return_value=systems,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
devices = await setup_integration(hass, config_entry, client)
|
||||
|
||||
entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
|
||||
assert len(entity_ids) == 1
|
||||
@@ -96,10 +77,10 @@ async def test_system_refresh_failure_marks_entities_unavailable(
|
||||
assert state.state == STATE_ON
|
||||
|
||||
async def fail_update() -> None:
|
||||
system.online = None
|
||||
raise AqualinkServiceException
|
||||
devices.system.online = None
|
||||
raise raised_exception
|
||||
|
||||
system.update = AsyncMock(side_effect=fail_update)
|
||||
devices.system.update = AsyncMock(side_effect=fail_update)
|
||||
|
||||
await _advance_coordinator_time(hass, freezer)
|
||||
|
||||
@@ -165,39 +146,16 @@ async def test_light_service_calls_update_entity_state(
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test light service calls update entity state from device properties."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
systems = {system.serial: system}
|
||||
light = get_aqualink_device(
|
||||
system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
|
||||
)
|
||||
devices = {light.name: light}
|
||||
system.get_devices = AsyncMock(return_value=devices)
|
||||
devices = await setup_integration(hass, config_entry, client)
|
||||
|
||||
async def turn_off() -> None:
|
||||
light.data["state"] = "0"
|
||||
devices.light.data["state"] = "0"
|
||||
|
||||
async def turn_on() -> None:
|
||||
light.data["state"] = "1"
|
||||
devices.light.data["state"] = "1"
|
||||
|
||||
light.turn_off = AsyncMock(side_effect=turn_off)
|
||||
light.turn_on = AsyncMock(side_effect=turn_on)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
return_value=systems,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
devices.light.turn_off = AsyncMock(side_effect=turn_off)
|
||||
devices.light.turn_on = AsyncMock(side_effect=turn_on)
|
||||
|
||||
entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
|
||||
assert len(entity_ids) == 1
|
||||
@@ -228,15 +186,25 @@ async def test_light_service_calls_update_entity_state(
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_setup_login_exception(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
@pytest.mark.parametrize(
|
||||
"raised_exception",
|
||||
[
|
||||
pytest.param(AqualinkServiceException, id="service"),
|
||||
pytest.param(TimeoutError, id="timeout"),
|
||||
pytest.param(httpx.HTTPError("boom"), id="http"),
|
||||
],
|
||||
)
|
||||
async def test_setup_login_retry_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
raised_exception: Exception | type[Exception],
|
||||
) -> None:
|
||||
"""Test setup encountering a login exception."""
|
||||
"""Test setup retries on connection-related login exceptions."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
side_effect=AqualinkServiceException,
|
||||
side_effect=raised_exception,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@@ -264,24 +232,18 @@ async def test_setup_login_unauthorized(
|
||||
assert flows[0]["context"]["source"] == SOURCE_REAUTH
|
||||
|
||||
|
||||
async def test_setup_login_timeout(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test setup encountering a timeout while logging in."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
side_effect=TimeoutError,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raised_exception",
|
||||
[
|
||||
pytest.param(AqualinkServiceException, id="service"),
|
||||
pytest.param(TimeoutError, id="timeout"),
|
||||
pytest.param(httpx.HTTPError("boom"), id="http"),
|
||||
],
|
||||
)
|
||||
async def test_setup_systems_exception(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
raised_exception: Exception | type[Exception],
|
||||
) -> None:
|
||||
"""Test setup encountering an exception while retrieving systems."""
|
||||
config_entry.add_to_hass(hass)
|
||||
@@ -293,7 +255,7 @@ async def test_setup_systems_exception(
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
side_effect=AqualinkServiceException,
|
||||
side_effect=raised_exception,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@@ -385,15 +347,25 @@ async def test_setup_no_systems_recognized(
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raised_exception",
|
||||
[
|
||||
pytest.param(AqualinkServiceException, id="service"),
|
||||
pytest.param(TimeoutError, id="timeout"),
|
||||
pytest.param(httpx.HTTPError("boom"), id="http"),
|
||||
],
|
||||
)
|
||||
async def test_setup_devices_exception(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
raised_exception: Exception | type[Exception],
|
||||
) -> None:
|
||||
"""Test setup encountering an exception while retrieving devices."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
systems = {system.serial: system}
|
||||
|
||||
@@ -411,13 +383,51 @@ async def test_setup_devices_exception(
|
||||
"get_devices",
|
||||
) as mock_get_devices,
|
||||
):
|
||||
mock_get_devices.side_effect = AqualinkServiceException
|
||||
mock_get_devices.side_effect = raised_exception
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_devices_unauthorized(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test setup encountering an unauthorized exception while retrieving devices."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
systems = {system.serial: system}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
return_value=systems,
|
||||
),
|
||||
patch.object(
|
||||
system,
|
||||
"get_devices",
|
||||
side_effect=AqualinkServiceUnauthorizedException,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
assert flows[0]["context"]["source"] == SOURCE_REAUTH
|
||||
|
||||
|
||||
async def test_setup_all_good_no_recognized_devices(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -473,56 +483,7 @@ async def test_setup_all_good_all_device_types(
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test setup ending in one device of each type recognized."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
systems = {system.serial: system}
|
||||
|
||||
devices = [
|
||||
get_aqualink_device(
|
||||
system, name="aux_1", cls=IaquaAuxSwitch, data={"state": "0"}
|
||||
),
|
||||
get_aqualink_device(
|
||||
system, name="freeze_protection", cls=IaquaBinarySensor, data={"state": "0"}
|
||||
),
|
||||
get_aqualink_device(
|
||||
system, name="aux_2", cls=IaquaLightSwitch, data={"state": "0"}
|
||||
),
|
||||
get_aqualink_device(system, name="ph", cls=IaquaSensor, data={"state": "7.2"}),
|
||||
get_aqualink_device(
|
||||
system, name="pool_set_point", cls=IaquaThermostat, data={"state": "0"}
|
||||
),
|
||||
]
|
||||
devices = {d.name: d for d in devices}
|
||||
|
||||
pool_heater = get_aqualink_device(
|
||||
system, name="pool_heater", cls=IaquaAuxSwitch, data={"state": "0"}
|
||||
)
|
||||
pool_temp = get_aqualink_device(
|
||||
system, name="pool_temp", cls=IaquaSensor, data={"state": "72"}
|
||||
)
|
||||
system.devices = {
|
||||
**{d.name: d for d in devices.values()},
|
||||
pool_heater.name: pool_heater,
|
||||
pool_temp.name: pool_temp,
|
||||
}
|
||||
|
||||
system.get_devices = AsyncMock(return_value=devices)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
return_value=systems,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
await setup_integration(hass, config_entry, client)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -557,32 +518,8 @@ async def test_multiple_updates(
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test all possible results of online status transition after update."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
systems = {system.serial: system}
|
||||
|
||||
light = get_aqualink_device(
|
||||
system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
|
||||
)
|
||||
devices = {light.name: light}
|
||||
|
||||
system.get_devices = AsyncMock(return_value=devices)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
return_value=systems,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
devices = await setup_integration(hass, config_entry, client)
|
||||
system = devices.system
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -683,31 +620,7 @@ async def test_entity_assumed_and_available(
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test assumed_state and_available properties for all values of online."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
systems = {system.serial: system}
|
||||
|
||||
light = get_aqualink_device(
|
||||
system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
|
||||
)
|
||||
devices = {light.name: light}
|
||||
system.get_devices = AsyncMock(return_value=devices)
|
||||
system.update = AsyncMock()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
return_value=systems,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
devices = await setup_integration(hass, config_entry, client)
|
||||
|
||||
entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
|
||||
assert len(entity_ids) == 1
|
||||
@@ -715,19 +628,19 @@ async def test_entity_assumed_and_available(
|
||||
name = entity_ids[0]
|
||||
|
||||
# None means maybe.
|
||||
light.system.online = None
|
||||
devices.light.system.online = None
|
||||
await _advance_coordinator_time(hass, freezer)
|
||||
state = hass.states.get(name)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
|
||||
|
||||
light.system.online = False
|
||||
devices.light.system.online = False
|
||||
await _advance_coordinator_time(hass, freezer)
|
||||
state = hass.states.get(name)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
|
||||
|
||||
light.system.online = True
|
||||
devices.light.system.online = True
|
||||
await _advance_coordinator_time(hass, freezer)
|
||||
state = hass.states.get(name)
|
||||
assert state.state == STATE_ON
|
||||
@@ -741,34 +654,11 @@ async def test_system_refresh_unauthorized_triggers_reauth(
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test an unauthorized refresh starts reauthentication."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
systems = {system.serial: system}
|
||||
|
||||
light = get_aqualink_device(
|
||||
system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
|
||||
)
|
||||
system.get_devices = AsyncMock(return_value={light.name: light})
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
return_value=None,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
return_value=systems,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
devices = await setup_integration(hass, config_entry, client)
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
system.update = AsyncMock(side_effect=AqualinkServiceUnauthorizedException)
|
||||
devices.system.update = AsyncMock(side_effect=AqualinkServiceUnauthorizedException)
|
||||
|
||||
await _advance_coordinator_time(hass, freezer)
|
||||
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
"""Light platform tests for iAquaLink."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
from iaqualink.systems.iaqua.device import (
|
||||
IaquaColorLightJC,
|
||||
IaquaDimmableLight,
|
||||
IaquaLightSwitch,
|
||||
)
|
||||
from iaqualink.systems.iaqua.system import IaquaSystem
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
ATTR_EFFECT,
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
ColorMode,
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import (
|
||||
assert_platform_setup,
|
||||
get_aqualink_device,
|
||||
get_aqualink_system,
|
||||
setup_entry,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test all light entities are created correctly."""
|
||||
await assert_platform_setup(
|
||||
hass, config_entry, client, entity_registry, snapshot, LIGHT_DOMAIN
|
||||
)
|
||||
|
||||
|
||||
async def _setup_light(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
cls: type,
|
||||
data: dict[str, str],
|
||||
) -> tuple[IaquaSystem, object, str, object]:
|
||||
"""Set up the integration with a single light entity."""
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
light = get_aqualink_device(system, name="aux_1", cls=cls, data=data)
|
||||
system.get_devices = AsyncMock(return_value={light.name: light})
|
||||
system.set_aux = AsyncMock()
|
||||
system.set_light = AsyncMock()
|
||||
|
||||
await setup_entry(hass, config_entry, system)
|
||||
|
||||
entity_ids = hass.states.async_entity_ids(LIGHT_DOMAIN)
|
||||
assert len(entity_ids) == 1
|
||||
entity_id = entity_ids[0]
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
return system, light, entity_id, entity_state
|
||||
|
||||
|
||||
async def test_effect_light_setup_exposes_effect_features(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test color lights expose effect-related state through Home Assistant."""
|
||||
_, _, _, entity_state = await _setup_light(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
IaquaColorLightJC,
|
||||
{"state": "1", "aux": "1", "subtype": "1", "label": "pool light"},
|
||||
)
|
||||
|
||||
assert entity_state.state == STATE_ON
|
||||
assert entity_state.attributes["color_mode"] == ColorMode.ONOFF.value
|
||||
assert entity_state.attributes["supported_features"] == LightEntityFeature.EFFECT
|
||||
assert "Alpine White" in entity_state.attributes["effect_list"]
|
||||
|
||||
|
||||
async def test_dimmable_light_setup_exposes_brightness(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test dimmable lights expose brightness through Home Assistant."""
|
||||
_, _, _, entity_state = await _setup_light(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
IaquaDimmableLight,
|
||||
{"state": "1", "aux": "1", "subtype": "50", "label": "pool light"},
|
||||
)
|
||||
|
||||
assert entity_state.state == STATE_ON
|
||||
assert entity_state.attributes["color_mode"] == ColorMode.BRIGHTNESS.value
|
||||
assert entity_state.attributes["brightness"] == 128
|
||||
|
||||
|
||||
async def test_light_turn_on_with_effect_updates_state(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test turning on a color light with an effect uses the effect branch."""
|
||||
system, light, entity_id, _ = await _setup_light(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
IaquaColorLightJC,
|
||||
{"state": "0", "aux": "1", "subtype": "1", "label": "pool light"},
|
||||
)
|
||||
light_component = hass.data[LIGHT_DOMAIN]
|
||||
entity = light_component.get_entity(entity_id)
|
||||
assert entity is not None
|
||||
|
||||
async def set_light(data: dict[str, str]) -> None:
|
||||
light.data["state"] = data["light"]
|
||||
|
||||
system.set_light = AsyncMock(side_effect=set_light)
|
||||
|
||||
await entity.async_turn_on(**{ATTR_EFFECT: "Alpine White"})
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_light_turn_on_with_brightness_updates_state(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test turning on a dimmable light updates Home Assistant state."""
|
||||
system, light, entity_id, _ = await _setup_light(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
IaquaDimmableLight,
|
||||
{"state": "0", "aux": "1", "subtype": "0", "label": "pool light"},
|
||||
)
|
||||
|
||||
async def set_light(data: dict[str, str]) -> None:
|
||||
light.data["state"] = "1" if data["light"] != "0" else "0"
|
||||
light.data["subtype"] = data["light"]
|
||||
|
||||
system.set_light = AsyncMock(side_effect=set_light)
|
||||
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_ON,
|
||||
{ATTR_ENTITY_ID: entity_id, ATTR_BRIGHTNESS: 128},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == STATE_ON
|
||||
assert entity_state.attributes["brightness"] == 128
|
||||
|
||||
|
||||
async def test_light_turn_on_without_attributes_updates_state(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
) -> None:
|
||||
"""Test turning on a basic light uses the default turn_on path."""
|
||||
system, light, entity_id, _ = await _setup_light(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
IaquaLightSwitch,
|
||||
{"state": "0", "aux": "1", "label": "pool light"},
|
||||
)
|
||||
|
||||
async def set_aux(_: str) -> None:
|
||||
light.data["state"] = "1"
|
||||
|
||||
system.set_aux = AsyncMock(side_effect=set_aux)
|
||||
light_component = hass.data[LIGHT_DOMAIN]
|
||||
entity = light_component.get_entity(entity_id)
|
||||
assert entity is not None
|
||||
|
||||
await entity.async_turn_on()
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raised_exception", "expected_exception", "match"),
|
||||
[
|
||||
pytest.param(
|
||||
AqualinkServiceException,
|
||||
HomeAssistantError,
|
||||
"Aqualink error: AqualinkServiceException",
|
||||
id="service",
|
||||
),
|
||||
pytest.param(
|
||||
TimeoutError(),
|
||||
HomeAssistantError,
|
||||
"Aqualink error: TimeoutError",
|
||||
id="timeout",
|
||||
),
|
||||
pytest.param(
|
||||
httpx.HTTPError("boom"),
|
||||
HomeAssistantError,
|
||||
"Aqualink error: boom",
|
||||
id="http",
|
||||
),
|
||||
pytest.param(
|
||||
AqualinkServiceUnauthorizedException,
|
||||
ConfigEntryAuthFailed,
|
||||
"Invalid credentials for iAquaLink",
|
||||
id="unauthorized",
|
||||
),
|
||||
pytest.param(
|
||||
Exception("Test exception"),
|
||||
Exception,
|
||||
"Test exception",
|
||||
id="unexpected",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_light_turn_off_errors_leave_state_unchanged(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
raised_exception: Exception | type[Exception],
|
||||
expected_exception: type[Exception],
|
||||
match: str,
|
||||
) -> None:
|
||||
"""Test turn-off errors are surfaced through the light service call."""
|
||||
system, _, entity_id, _ = await _setup_light(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
IaquaLightSwitch,
|
||||
{"state": "1", "aux": "1", "label": "pool light"},
|
||||
)
|
||||
system.set_aux = AsyncMock(side_effect=raised_exception)
|
||||
|
||||
with pytest.raises(expected_exception, match=match):
|
||||
await hass.services.async_call(
|
||||
LIGHT_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == STATE_ON
|
||||
@@ -0,0 +1,148 @@
|
||||
"""Sensor platform tests for iAquaLink."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.systems.iaqua.device import IaquaSensor
|
||||
from iaqualink.systems.iaqua.system import IaquaSystem
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.const import STATE_UNKNOWN, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
|
||||
|
||||
from .conftest import (
|
||||
assert_platform_setup,
|
||||
get_aqualink_device,
|
||||
get_aqualink_system,
|
||||
setup_entry,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test all sensor entities are created correctly."""
|
||||
await assert_platform_setup(
|
||||
hass, config_entry, client, entity_registry, snapshot, SENSOR_DOMAIN
|
||||
)
|
||||
|
||||
|
||||
async def _setup_sensor(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
*,
|
||||
name: str,
|
||||
state: str,
|
||||
temp_unit: str = "F",
|
||||
) -> tuple[str, object]:
|
||||
"""Set up the integration with a single sensor entity."""
|
||||
system = get_aqualink_system(
|
||||
client,
|
||||
cls=IaquaSystem,
|
||||
data={"home_screen": [{}, {}, {}, {"temp_scale": temp_unit}]},
|
||||
)
|
||||
system.online = True
|
||||
|
||||
async def update() -> None:
|
||||
system.temp_unit = temp_unit
|
||||
|
||||
system.update = AsyncMock(side_effect=update)
|
||||
sensor = get_aqualink_device(
|
||||
system, name=name, cls=IaquaSensor, data={"state": state}
|
||||
)
|
||||
system.get_devices = AsyncMock(return_value={sensor.name: sensor})
|
||||
|
||||
await setup_entry(hass, config_entry, system)
|
||||
|
||||
entity_ids = hass.states.async_entity_ids(SENSOR_DOMAIN)
|
||||
assert len(entity_ids) == 1
|
||||
entity_id = entity_ids[0]
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
return entity_id, entity_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("name", "temp_unit", "expected_class", "expected_unit"),
|
||||
[
|
||||
pytest.param(
|
||||
"pool_temp",
|
||||
"F",
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
id="fahrenheit-temperature",
|
||||
),
|
||||
pytest.param(
|
||||
"spa_temp",
|
||||
"C",
|
||||
SensorDeviceClass.TEMPERATURE,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
id="celsius-temperature",
|
||||
),
|
||||
pytest.param("ph", "F", None, None, id="non-temperature"),
|
||||
],
|
||||
)
|
||||
async def test_sensor_initialization(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
name: str,
|
||||
temp_unit: str,
|
||||
expected_class: SensorDeviceClass | None,
|
||||
expected_unit: UnitOfTemperature | None,
|
||||
) -> None:
|
||||
"""Test sensor initialization for temperature and generic sensors."""
|
||||
if expected_unit is UnitOfTemperature.FAHRENHEIT:
|
||||
hass.config.units = US_CUSTOMARY_SYSTEM
|
||||
|
||||
_, entity_state = await _setup_sensor(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
name=name,
|
||||
temp_unit=temp_unit,
|
||||
state="72",
|
||||
)
|
||||
|
||||
assert entity_state.attributes.get("device_class") == expected_class
|
||||
assert entity_state.attributes.get("unit_of_measurement") == expected_unit
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("state", "expected_state"),
|
||||
[
|
||||
pytest.param("", STATE_UNKNOWN, id="empty"),
|
||||
pytest.param("72", "72", id="int"),
|
||||
pytest.param("7.2", "7.2", id="float"),
|
||||
],
|
||||
)
|
||||
async def test_sensor_native_value(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
state: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test sensor state parsing."""
|
||||
_, entity_state = await _setup_sensor(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
name="ph",
|
||||
state=state,
|
||||
)
|
||||
|
||||
assert entity_state.state == expected_state
|
||||
@@ -0,0 +1,214 @@
|
||||
"""Switch platform tests for iAquaLink."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
from iaqualink.systems.iaqua.device import IaquaAuxSwitch
|
||||
from iaqualink.systems.iaqua.system import IaquaSystem
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
SERVICE_TURN_OFF,
|
||||
SERVICE_TURN_ON,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import (
|
||||
assert_platform_setup,
|
||||
get_aqualink_device,
|
||||
get_aqualink_system,
|
||||
setup_entry,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test all switch entities are created correctly."""
|
||||
await assert_platform_setup(
|
||||
hass, config_entry, client, entity_registry, snapshot, SWITCH_DOMAIN
|
||||
)
|
||||
|
||||
|
||||
async def _setup_switch(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
*,
|
||||
label: str,
|
||||
state: str,
|
||||
) -> tuple[IaquaSystem, object, str, object]:
|
||||
"""Set up the integration with a single switch entity."""
|
||||
system = get_aqualink_system(client, cls=IaquaSystem)
|
||||
system.online = True
|
||||
system.update = AsyncMock()
|
||||
switch = get_aqualink_device(
|
||||
system,
|
||||
name=label.lower().replace(" ", "_"),
|
||||
cls=IaquaAuxSwitch,
|
||||
data={"state": state, "aux": "1", "label": label},
|
||||
)
|
||||
system.get_devices = AsyncMock(return_value={switch.name: switch})
|
||||
system.set_aux = AsyncMock()
|
||||
|
||||
await setup_entry(hass, config_entry, system)
|
||||
|
||||
entity_ids = hass.states.async_entity_ids(SWITCH_DOMAIN)
|
||||
assert len(entity_ids) == 1
|
||||
entity_id = entity_ids[0]
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
return system, switch, entity_id, entity_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("label", "expected_icon"),
|
||||
[
|
||||
pytest.param("Cleaner", "mdi:robot-vacuum", id="cleaner"),
|
||||
pytest.param("Waterfall", "mdi:fountain", id="waterfall"),
|
||||
pytest.param("Spa Dscnt", "mdi:fountain", id="descent"),
|
||||
pytest.param("Filter Pump", "mdi:fan", id="pump"),
|
||||
pytest.param("Spa Blower", "mdi:fan", id="blower"),
|
||||
pytest.param("Pool Heater", "mdi:radiator", id="heater"),
|
||||
pytest.param("Auxiliary", None, id="default"),
|
||||
],
|
||||
)
|
||||
async def test_switch_icons(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
label: str,
|
||||
expected_icon: str | None,
|
||||
) -> None:
|
||||
"""Test switch icons are derived from the device label."""
|
||||
_, _, _, entity_state = await _setup_switch(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
label=label,
|
||||
state="1",
|
||||
)
|
||||
|
||||
assert entity_state.attributes.get("icon") == expected_icon
|
||||
assert entity_state.state == STATE_ON
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "initial_state", "expected_state"),
|
||||
[
|
||||
pytest.param(SERVICE_TURN_ON, "0", STATE_ON, id="turn-on"),
|
||||
pytest.param(SERVICE_TURN_OFF, "1", STATE_OFF, id="turn-off"),
|
||||
],
|
||||
)
|
||||
async def test_switch_actions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
service: str,
|
||||
initial_state: str,
|
||||
expected_state: str,
|
||||
) -> None:
|
||||
"""Test switch services update Home Assistant state."""
|
||||
system, switch, entity_id, _ = await _setup_switch(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
label="Auxiliary",
|
||||
state=initial_state,
|
||||
)
|
||||
|
||||
async def set_aux(_: str) -> None:
|
||||
switch.data["state"] = "1" if service == SERVICE_TURN_ON else "0"
|
||||
|
||||
system.set_aux = AsyncMock(side_effect=set_aux)
|
||||
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
service,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == expected_state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raised_exception", "expected_exception", "match"),
|
||||
[
|
||||
pytest.param(
|
||||
AqualinkServiceException,
|
||||
HomeAssistantError,
|
||||
"Aqualink error: AqualinkServiceException",
|
||||
id="service",
|
||||
),
|
||||
pytest.param(
|
||||
TimeoutError(),
|
||||
HomeAssistantError,
|
||||
"Aqualink error: TimeoutError",
|
||||
id="timeout",
|
||||
),
|
||||
pytest.param(
|
||||
httpx.HTTPError("boom"),
|
||||
HomeAssistantError,
|
||||
"Aqualink error: boom",
|
||||
id="http",
|
||||
),
|
||||
pytest.param(
|
||||
AqualinkServiceUnauthorizedException,
|
||||
ConfigEntryAuthFailed,
|
||||
"Invalid credentials for iAquaLink",
|
||||
id="unauthorized",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_switch_turn_off_errors_leave_state_unchanged(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
client: AqualinkClient,
|
||||
raised_exception: Exception | type[Exception],
|
||||
expected_exception: type[Exception],
|
||||
match: str,
|
||||
) -> None:
|
||||
"""Test turn-off errors are surfaced through the switch service call."""
|
||||
system, _, entity_id, _ = await _setup_switch(
|
||||
hass,
|
||||
config_entry,
|
||||
client,
|
||||
label="Auxiliary",
|
||||
state="1",
|
||||
)
|
||||
system.set_aux = AsyncMock(side_effect=raised_exception)
|
||||
|
||||
with pytest.raises(expected_exception, match=match):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
entity_state = hass.states.get(entity_id)
|
||||
assert entity_state is not None
|
||||
assert entity_state.state == STATE_ON
|
||||
@@ -1,24 +0,0 @@
|
||||
"""Tests for iAquaLink integration utility functions."""
|
||||
|
||||
from iaqualink.exception import AqualinkServiceException
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.iaqualink.utils import await_or_reraise
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .conftest import async_raises, async_returns
|
||||
|
||||
|
||||
async def test_await_or_reraise(hass: HomeAssistant) -> None:
|
||||
"""Test await_or_reraise for all values of awaitable."""
|
||||
async_noop = async_returns(None)
|
||||
await await_or_reraise(async_noop())
|
||||
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await await_or_reraise(async_raises(Exception("Test exception"))())
|
||||
assert str(exc_info.value) == "Test exception"
|
||||
|
||||
async_ex = async_raises(AqualinkServiceException)
|
||||
with pytest.raises(HomeAssistantError):
|
||||
await await_or_reraise(async_ex())
|
||||
Reference in New Issue
Block a user