Update to iaqualink 0.4.1 (#53745)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
5824477298
commit
3c2d5d5f8c
14 changed files with 580 additions and 65 deletions
|
@ -6,16 +6,16 @@ from functools import wraps
|
|||
import logging
|
||||
|
||||
import aiohttp.client_exceptions
|
||||
from iaqualink import (
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.device import (
|
||||
AqualinkBinarySensor,
|
||||
AqualinkClient,
|
||||
AqualinkDevice,
|
||||
AqualinkLight,
|
||||
AqualinkLoginException,
|
||||
AqualinkSensor,
|
||||
AqualinkThermostat,
|
||||
AqualinkToggle,
|
||||
)
|
||||
from iaqualink.exception import AqualinkServiceException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
@ -73,12 +73,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||
"""Set up the Aqualink component."""
|
||||
conf = config.get(DOMAIN)
|
||||
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
if conf is not None:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data=conf,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -90,6 +90,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
username = entry.data[CONF_USERNAME]
|
||||
password = entry.data[CONF_PASSWORD]
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
# These will contain the initialized devices
|
||||
binary_sensors = hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] = []
|
||||
climates = hass.data[DOMAIN][CLIMATE_DOMAIN] = []
|
||||
|
@ -101,24 +103,36 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
aqualink = AqualinkClient(username, password, session)
|
||||
try:
|
||||
await aqualink.login()
|
||||
except AqualinkLoginException as login_exception:
|
||||
except AqualinkServiceException as login_exception:
|
||||
_LOGGER.error("Failed to login: %s", login_exception)
|
||||
return False
|
||||
except (
|
||||
asyncio.TimeoutError,
|
||||
aiohttp.client_exceptions.ClientConnectorError,
|
||||
) as aio_exception:
|
||||
_LOGGER.warning("Exception raised while attempting to login: %s", aio_exception)
|
||||
raise ConfigEntryNotReady from aio_exception
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while attempting login: {aio_exception}"
|
||||
) from aio_exception
|
||||
|
||||
try:
|
||||
systems = await aqualink.get_systems()
|
||||
except AqualinkServiceException as svc_exception:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while attempting to retrieve systems list: {svc_exception}"
|
||||
) from svc_exception
|
||||
|
||||
systems = await aqualink.get_systems()
|
||||
systems = list(systems.values())
|
||||
if not systems:
|
||||
_LOGGER.error("No systems detected or supported")
|
||||
return False
|
||||
|
||||
# Only supporting the first system for now.
|
||||
devices = await systems[0].get_devices()
|
||||
try:
|
||||
devices = await systems[0].get_devices()
|
||||
except AqualinkServiceException as svc_exception:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error while attempting to retrieve devices list: {svc_exception}"
|
||||
) from svc_exception
|
||||
|
||||
for dev in devices.values():
|
||||
if isinstance(dev, AqualinkThermostat):
|
||||
|
@ -151,15 +165,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
|
||||
async def _async_systems_update(now):
|
||||
"""Refresh internal state for all systems."""
|
||||
prev = systems[0].last_run_success
|
||||
prev = systems[0].online
|
||||
|
||||
await systems[0].update()
|
||||
success = systems[0].last_run_success
|
||||
|
||||
if not success and prev:
|
||||
_LOGGER.warning("Failed to refresh iAqualink state")
|
||||
elif success and not prev:
|
||||
_LOGGER.warning("Reconnected to iAqualink")
|
||||
try:
|
||||
await systems[0].update()
|
||||
except AqualinkServiceException as svc_exception:
|
||||
if prev is not None:
|
||||
_LOGGER.warning("Failed to refresh iAqualink state: %s", svc_exception)
|
||||
else:
|
||||
cur = systems[0].online
|
||||
if cur is True and prev is not True:
|
||||
_LOGGER.warning("Reconnected to iAqualink")
|
||||
|
||||
async_dispatcher_send(hass, DOMAIN)
|
||||
|
||||
|
@ -174,7 +190,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
platform for platform in PLATFORMS if platform in hass.data[DOMAIN]
|
||||
]
|
||||
|
||||
hass.data[DOMAIN].clear()
|
||||
del hass.data[DOMAIN]
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, platforms_to_unload)
|
||||
|
||||
|
@ -228,12 +244,12 @@ class AqualinkEntity(Entity):
|
|||
@property
|
||||
def assumed_state(self) -> bool:
|
||||
"""Return whether the state is based on actual reading from the device."""
|
||||
return not self.dev.system.last_run_success
|
||||
return self.dev.system.online in [False, None]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return whether the device is available or not."""
|
||||
return self.dev.system.online
|
||||
return self.dev.system.online is True
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
|
|
|
@ -3,13 +3,13 @@ from __future__ import annotations
|
|||
|
||||
import logging
|
||||
|
||||
from iaqualink import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState
|
||||
from iaqualink.const import (
|
||||
AQUALINK_TEMP_CELSIUS_HIGH,
|
||||
AQUALINK_TEMP_CELSIUS_LOW,
|
||||
AQUALINK_TEMP_FAHRENHEIT_HIGH,
|
||||
AQUALINK_TEMP_FAHRENHEIT_LOW,
|
||||
)
|
||||
from iaqualink.device import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState
|
||||
|
||||
from homeassistant.components.climate import ClimateEntity
|
||||
from homeassistant.components.climate.const import (
|
||||
|
@ -24,6 +24,7 @@ from homeassistant.core import HomeAssistant
|
|||
|
||||
from . import AqualinkEntity, refresh_system
|
||||
from .const import CLIMATE_SUPPORTED_MODES, DOMAIN as AQUALINK_DOMAIN
|
||||
from .utils import await_or_reraise
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -76,9 +77,9 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
|
|||
async def async_set_hvac_mode(self, hvac_mode: str) -> None:
|
||||
"""Turn the underlying heater switch on or off."""
|
||||
if hvac_mode == HVAC_MODE_HEAT:
|
||||
await self.heater.turn_on()
|
||||
await await_or_reraise(self.heater.turn_on())
|
||||
elif hvac_mode == HVAC_MODE_OFF:
|
||||
await self.heater.turn_off()
|
||||
await await_or_reraise(self.heater.turn_off())
|
||||
else:
|
||||
_LOGGER.warning("Unknown operation mode: %s", hvac_mode)
|
||||
|
||||
|
@ -111,7 +112,7 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
|
|||
@refresh_system
|
||||
async def async_set_temperature(self, **kwargs) -> None:
|
||||
"""Set new target temperature."""
|
||||
await self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE]))
|
||||
await await_or_reraise(self.dev.set_temperature(int(kwargs[ATTR_TEMPERATURE])))
|
||||
|
||||
@property
|
||||
def sensor(self) -> AqualinkSensor:
|
||||
|
|
|
@ -3,7 +3,11 @@ from __future__ import annotations
|
|||
|
||||
from typing import Any
|
||||
|
||||
from iaqualink import AqualinkClient, AqualinkLoginException
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
|
@ -32,16 +36,22 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
password = user_input[CONF_PASSWORD]
|
||||
|
||||
try:
|
||||
aqualink = AqualinkClient(username, password)
|
||||
await aqualink.login()
|
||||
return self.async_create_entry(title=username, data=user_input)
|
||||
except AqualinkLoginException:
|
||||
async with AqualinkClient(username, password):
|
||||
pass
|
||||
except AqualinkServiceUnauthorizedException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except AqualinkServiceException:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(title=username, data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
|
||||
{
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""Support for Aqualink pool lights."""
|
||||
from iaqualink import AqualinkLightEffect
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
|
@ -14,6 +14,9 @@ from homeassistant.core import HomeAssistant
|
|||
|
||||
from . import AqualinkEntity, refresh_system
|
||||
from .const import DOMAIN as AQUALINK_DOMAIN
|
||||
from .utils import await_or_reraise
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
@ -49,20 +52,19 @@ class HassAqualinkLight(AqualinkEntity, LightEntity):
|
|||
them.
|
||||
"""
|
||||
# For now I'm assuming lights support either effects or brightness.
|
||||
if effect := kwargs.get(ATTR_EFFECT):
|
||||
effect = AqualinkLightEffect[effect].value
|
||||
await self.dev.set_effect(effect)
|
||||
if effect_name := kwargs.get(ATTR_EFFECT):
|
||||
await await_or_reraise(self.dev.set_effect_by_name(effect_name))
|
||||
elif brightness := kwargs.get(ATTR_BRIGHTNESS):
|
||||
# Aqualink supports percentages in 25% increments.
|
||||
pct = int(round(brightness * 4.0 / 255)) * 25
|
||||
await self.dev.set_brightness(pct)
|
||||
await await_or_reraise(self.dev.set_brightness(pct))
|
||||
else:
|
||||
await self.dev.turn_on()
|
||||
await await_or_reraise(self.dev.turn_on())
|
||||
|
||||
@refresh_system
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn off the light."""
|
||||
await self.dev.turn_off()
|
||||
await await_or_reraise(self.dev.turn_off())
|
||||
|
||||
@property
|
||||
def brightness(self) -> int:
|
||||
|
@ -75,12 +77,12 @@ class HassAqualinkLight(AqualinkEntity, LightEntity):
|
|||
@property
|
||||
def effect(self) -> str:
|
||||
"""Return the current light effect if supported."""
|
||||
return AqualinkLightEffect(self.dev.effect).name
|
||||
return self.dev.effect
|
||||
|
||||
@property
|
||||
def effect_list(self) -> list:
|
||||
"""Return supported light effects."""
|
||||
return list(AqualinkLightEffect.__members__)
|
||||
return list(self.dev.supported_light_effects)
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
|
|
|
@ -4,6 +4,6 @@
|
|||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/iaqualink/",
|
||||
"codeowners": ["@flz"],
|
||||
"requirements": ["iaqualink==0.3.90"],
|
||||
"requirements": ["iaqualink==0.4.1"],
|
||||
"iot_class": "cloud_polling"
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@
|
|||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
|
||||
|
|
|
@ -5,6 +5,7 @@ from homeassistant.core import HomeAssistant
|
|||
|
||||
from . import AqualinkEntity, refresh_system
|
||||
from .const import DOMAIN as AQUALINK_DOMAIN
|
||||
from .utils import await_or_reraise
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
@ -47,9 +48,9 @@ class HassAqualinkSwitch(AqualinkEntity, SwitchEntity):
|
|||
@refresh_system
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
"""Turn on the switch."""
|
||||
await self.dev.turn_on()
|
||||
await await_or_reraise(self.dev.turn_on())
|
||||
|
||||
@refresh_system
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
"""Turn off the switch."""
|
||||
await self.dev.turn_off()
|
||||
await await_or_reraise(self.dev.turn_off())
|
||||
|
|
16
homeassistant/components/iaqualink/utils.py
Normal file
16
homeassistant/components/iaqualink/utils.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""Utility functions for Aqualink devices."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
|
||||
from iaqualink.exception import AqualinkServiceException
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
async def await_or_reraise(awaitable: Awaitable) -> None:
|
||||
"""Execute API call while catching service exceptions."""
|
||||
try:
|
||||
await awaitable
|
||||
except AqualinkServiceException as svc_exception:
|
||||
raise HomeAssistantError(f"Aqualink error: {svc_exception}") from svc_exception
|
|
@ -869,7 +869,7 @@ hyperion-py==0.7.4
|
|||
iammeter==0.1.7
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
iaqualink==0.3.90
|
||||
iaqualink==0.4.1
|
||||
|
||||
# homeassistant.components.watson_tts
|
||||
ibm-watson==5.2.2
|
||||
|
|
|
@ -550,7 +550,7 @@ huisbaasje-client==0.1.0
|
|||
hyperion-py==0.7.4
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
iaqualink==0.3.90
|
||||
iaqualink==0.4.1
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
|
|
82
tests/components/iaqualink/conftest.py
Normal file
82
tests/components/iaqualink/conftest.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
"""Configuration for iAqualink tests."""
|
||||
import random
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from iaqualink.client import AqualinkClient
|
||||
from iaqualink.device import AqualinkDevice
|
||||
from iaqualink.system import AqualinkSystem
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.iaqualink import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
MOCK_USERNAME = "test@example.com"
|
||||
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)
|
||||
|
||||
|
||||
def async_raises(x):
|
||||
"""Return exception-raising async mock."""
|
||||
return AsyncMock(side_effect=x)
|
||||
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def client_fixture():
|
||||
"""Create client fixture."""
|
||||
return AqualinkClient(username=MOCK_USERNAME, password=MOCK_PASSWORD)
|
||||
|
||||
|
||||
def get_aqualink_system(aqualink, cls=None, data=None):
|
||||
"""Create aqualink system."""
|
||||
if cls is None:
|
||||
cls = AqualinkSystem
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
num = random.randint(0, 99999)
|
||||
data["serial_number"] = f"SN{num:05}"
|
||||
|
||||
return cls(aqualink=aqualink, data=data)
|
||||
|
||||
|
||||
def get_aqualink_device(system, cls=None, data=None):
|
||||
"""Create aqualink device."""
|
||||
if cls is None:
|
||||
cls = AqualinkDevice
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
|
||||
num = random.randint(0, 999)
|
||||
data["name"] = f"name_{num:03}"
|
||||
|
||||
return cls(system=system, data=data)
|
||||
|
||||
|
||||
@pytest.fixture(name="config_data")
|
||||
def config_data_fixture():
|
||||
"""Create hass config fixture."""
|
||||
return MOCK_DATA
|
||||
|
||||
|
||||
@pytest.fixture(name="config")
|
||||
def config_fixture():
|
||||
"""Create hass config fixture."""
|
||||
return {DOMAIN: MOCK_DATA}
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
def config_entry_fixture():
|
||||
"""Create a mock HEOS config entry."""
|
||||
return MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=MOCK_DATA,
|
||||
)
|
|
@ -1,20 +1,19 @@
|
|||
"""Tests for iAqualink config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
import iaqualink
|
||||
from iaqualink.exception import (
|
||||
AqualinkServiceException,
|
||||
AqualinkServiceUnauthorizedException,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.iaqualink import config_flow
|
||||
|
||||
from tests.common import MockConfigEntry, mock_coro
|
||||
|
||||
DATA = {"username": "test@example.com", "password": "pass"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["import", "user"])
|
||||
async def test_already_configured(hass, step):
|
||||
async def test_already_configured(hass, config_entry, config_data, step):
|
||||
"""Test config flow when iaqualink component is already setup."""
|
||||
MockConfigEntry(domain="iaqualink", data=DATA).add_to_hass(hass)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
flow = config_flow.AqualinkFlowHandler()
|
||||
flow.hass = hass
|
||||
|
@ -22,14 +21,14 @@ async def test_already_configured(hass, step):
|
|||
|
||||
fname = f"async_step_{step}"
|
||||
func = getattr(flow, fname)
|
||||
result = await func(DATA)
|
||||
result = await func(config_data)
|
||||
|
||||
assert result["type"] == "abort"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["import", "user"])
|
||||
async def test_without_config(hass, step):
|
||||
"""Test with no configuration."""
|
||||
"""Test config flow with no configuration."""
|
||||
flow = config_flow.AqualinkFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {}
|
||||
|
@ -44,7 +43,7 @@ async def test_without_config(hass, step):
|
|||
|
||||
|
||||
@pytest.mark.parametrize("step", ["import", "user"])
|
||||
async def test_with_invalid_credentials(hass, step):
|
||||
async def test_with_invalid_credentials(hass, config_data, step):
|
||||
"""Test config flow with invalid username and/or password."""
|
||||
flow = config_flow.AqualinkFlowHandler()
|
||||
flow.hass = hass
|
||||
|
@ -52,9 +51,29 @@ async def test_with_invalid_credentials(hass, step):
|
|||
fname = f"async_step_{step}"
|
||||
func = getattr(flow, fname)
|
||||
with patch(
|
||||
"iaqualink.AqualinkClient.login", side_effect=iaqualink.AqualinkLoginException
|
||||
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
|
||||
side_effect=AqualinkServiceUnauthorizedException,
|
||||
):
|
||||
result = await func(DATA)
|
||||
result = await func(config_data)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("step", ["import", "user"])
|
||||
async def test_service_exception(hass, config_data, step):
|
||||
"""Test config flow encountering service exception."""
|
||||
flow = config_flow.AqualinkFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
fname = f"async_step_{step}"
|
||||
func = getattr(flow, fname)
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
|
||||
side_effect=AqualinkServiceException,
|
||||
):
|
||||
result = await func(config_data)
|
||||
|
||||
assert result["type"] == "form"
|
||||
assert result["step_id"] == "user"
|
||||
|
@ -62,17 +81,20 @@ async def test_with_invalid_credentials(hass, step):
|
|||
|
||||
|
||||
@pytest.mark.parametrize("step", ["import", "user"])
|
||||
async def test_with_existing_config(hass, step):
|
||||
"""Test with existing configuration."""
|
||||
async def test_with_existing_config(hass, config_data, step):
|
||||
"""Test config flow with existing configuration."""
|
||||
flow = config_flow.AqualinkFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {}
|
||||
|
||||
fname = f"async_step_{step}"
|
||||
func = getattr(flow, fname)
|
||||
with patch("iaqualink.AqualinkClient.login", return_value=mock_coro(None)):
|
||||
result = await func(DATA)
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
|
||||
return_value=None,
|
||||
):
|
||||
result = await func(config_data)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == DATA["username"]
|
||||
assert result["data"] == DATA
|
||||
assert result["title"] == config_data["username"]
|
||||
assert result["data"] == config_data
|
||||
|
|
341
tests/components/iaqualink/test_init.py
Normal file
341
tests/components/iaqualink/test_init.py
Normal file
|
@ -0,0 +1,341 @@
|
|||
"""Tests for iAqualink integration."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from iaqualink.device import (
|
||||
AqualinkAuxToggle,
|
||||
AqualinkBinarySensor,
|
||||
AqualinkDevice,
|
||||
AqualinkLightToggle,
|
||||
AqualinkSensor,
|
||||
AqualinkThermostat,
|
||||
)
|
||||
from iaqualink.exception import AqualinkServiceException
|
||||
|
||||
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.iaqualink.const import UPDATE_INTERVAL
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ASSUMED_STATE, STATE_ON, STATE_UNAVAILABLE
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.components.iaqualink.conftest import get_aqualink_device, get_aqualink_system
|
||||
|
||||
|
||||
async def _ffwd_next_update_interval(hass):
|
||||
now = dt_util.utcnow()
|
||||
async_fire_time_changed(hass, now + UPDATE_INTERVAL)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_setup_login_exception(hass, config_entry):
|
||||
"""Test setup encountering a login exception."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
side_effect=AqualinkServiceException,
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_setup_login_timeout(hass, config_entry):
|
||||
"""Test setup encountering a timeout while logging in."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login",
|
||||
side_effect=asyncio.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
|
||||
|
||||
|
||||
async def test_setup_systems_exception(hass, config_entry):
|
||||
"""Test setup encountering an exception while retrieving systems."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.login", return_value=None
|
||||
), patch(
|
||||
"homeassistant.components.iaqualink.AqualinkClient.get_systems",
|
||||
side_effect=AqualinkServiceException,
|
||||
):
|
||||
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_no_systems_recognized(hass, config_entry):
|
||||
"""Test setup ending in no systems recognized."""
|
||||
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={},
|
||||
):
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_setup_devices_exception(hass, config_entry, client):
|
||||
"""Test setup encountering an exception while retrieving devices."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client)
|
||||
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"
|
||||
) as mock_get_devices:
|
||||
mock_get_devices.side_effect = AqualinkServiceException
|
||||
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_all_good_no_recognized_devices(hass, config_entry, client):
|
||||
"""Test setup ending in no devices recognized."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client)
|
||||
systems = {system.serial: system}
|
||||
|
||||
device = get_aqualink_device(system, AqualinkDevice)
|
||||
devices = {device.name: device}
|
||||
|
||||
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"
|
||||
) as mock_get_devices:
|
||||
mock_get_devices.return_value = devices
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0
|
||||
assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 0
|
||||
assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 0
|
||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 0
|
||||
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 0
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_setup_all_good_all_device_types(hass, config_entry, client):
|
||||
"""Test setup ending in one device of each type recognized."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client)
|
||||
systems = {system.serial: system}
|
||||
|
||||
devices = [
|
||||
get_aqualink_device(system, AqualinkAuxToggle),
|
||||
get_aqualink_device(system, AqualinkBinarySensor),
|
||||
get_aqualink_device(system, AqualinkLightToggle),
|
||||
get_aqualink_device(system, AqualinkSensor),
|
||||
get_aqualink_device(system, AqualinkThermostat),
|
||||
]
|
||||
devices = {d.name: d for d in devices}
|
||||
|
||||
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()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1
|
||||
assert len(hass.states.async_entity_ids(CLIMATE_DOMAIN)) == 1
|
||||
assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1
|
||||
assert len(hass.states.async_entity_ids(SENSOR_DOMAIN)) == 1
|
||||
assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 1
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_multiple_updates(hass, config_entry, caplog, client):
|
||||
"""Test all possible results of online status transition after update."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client)
|
||||
systems = {system.serial: system}
|
||||
|
||||
system.get_devices = AsyncMock(return_value={})
|
||||
|
||||
caplog.set_level(logging.WARNING)
|
||||
|
||||
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()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
def set_online_to_true():
|
||||
system.online = True
|
||||
|
||||
def set_online_to_false():
|
||||
system.online = False
|
||||
|
||||
system.update = AsyncMock()
|
||||
|
||||
# True -> True
|
||||
system.online = True
|
||||
caplog.clear()
|
||||
system.update.side_effect = set_online_to_true
|
||||
await _ffwd_next_update_interval(hass)
|
||||
assert len(caplog.records) == 0
|
||||
|
||||
# True -> False
|
||||
system.online = True
|
||||
caplog.clear()
|
||||
system.update.side_effect = set_online_to_false
|
||||
await _ffwd_next_update_interval(hass)
|
||||
assert len(caplog.records) == 0
|
||||
|
||||
# True -> None / ServiceException
|
||||
system.online = True
|
||||
caplog.clear()
|
||||
system.update.side_effect = AqualinkServiceException
|
||||
await _ffwd_next_update_interval(hass)
|
||||
assert len(caplog.records) == 1
|
||||
assert "Failed" in caplog.text
|
||||
|
||||
# False -> False
|
||||
system.online = False
|
||||
caplog.clear()
|
||||
system.update.side_effect = set_online_to_false
|
||||
await _ffwd_next_update_interval(hass)
|
||||
assert len(caplog.records) == 0
|
||||
|
||||
# False -> True
|
||||
system.online = False
|
||||
caplog.clear()
|
||||
system.update.side_effect = set_online_to_true
|
||||
await _ffwd_next_update_interval(hass)
|
||||
assert len(caplog.records) == 1
|
||||
assert "Reconnected" in caplog.text
|
||||
|
||||
# False -> None / ServiceException
|
||||
system.online = False
|
||||
caplog.clear()
|
||||
system.update.side_effect = AqualinkServiceException
|
||||
await _ffwd_next_update_interval(hass)
|
||||
assert len(caplog.records) == 1
|
||||
assert "Failed" in caplog.text
|
||||
|
||||
# None -> None / ServiceException
|
||||
system.online = None
|
||||
caplog.clear()
|
||||
system.update.side_effect = AqualinkServiceException
|
||||
await _ffwd_next_update_interval(hass)
|
||||
assert len(caplog.records) == 0
|
||||
|
||||
# None -> True
|
||||
system.online = None
|
||||
caplog.clear()
|
||||
system.update.side_effect = set_online_to_true
|
||||
await _ffwd_next_update_interval(hass)
|
||||
assert len(caplog.records) == 1
|
||||
assert "Reconnected" in caplog.text
|
||||
|
||||
# None -> False
|
||||
system.online = None
|
||||
caplog.clear()
|
||||
system.update.side_effect = set_online_to_false
|
||||
await _ffwd_next_update_interval(hass)
|
||||
assert len(caplog.records) == 0
|
||||
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_entity_assumed_and_available(hass, config_entry, client):
|
||||
"""Test assumed_state and_available properties for all values of online."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
system = get_aqualink_system(client)
|
||||
systems = {system.serial: system}
|
||||
|
||||
light = get_aqualink_device(system, AqualinkLightToggle, data={"state": "1"})
|
||||
devices = {d.name: d for d in [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()
|
||||
|
||||
assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1
|
||||
|
||||
name = f"{LIGHT_DOMAIN}.{light.name}"
|
||||
|
||||
# None means maybe.
|
||||
light.system.online = None
|
||||
await _ffwd_next_update_interval(hass)
|
||||
state = hass.states.get(name)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
|
||||
|
||||
light.system.online = False
|
||||
await _ffwd_next_update_interval(hass)
|
||||
state = hass.states.get(name)
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
assert state.attributes.get(ATTR_ASSUMED_STATE) is True
|
||||
|
||||
light.system.online = True
|
||||
await _ffwd_next_update_interval(hass)
|
||||
state = hass.states.get(name)
|
||||
assert state.state == STATE_ON
|
||||
assert state.attributes.get(ATTR_ASSUMED_STATE) is None
|
23
tests/components/iaqualink/test_utils.py
Normal file
23
tests/components/iaqualink/test_utils.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
"""Tests for iAqualink integration utility functions."""
|
||||
|
||||
from iaqualink.exception import AqualinkServiceException
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.iaqualink.utils import await_or_reraise
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from tests.components.iaqualink.conftest import async_raises, async_returns
|
||||
|
||||
|
||||
async def test_await_or_reraise(hass):
|
||||
"""Test await_or_reraise for all values of awaitable."""
|
||||
async_noop = async_returns(None)
|
||||
await await_or_reraise(async_noop())
|
||||
|
||||
with pytest.raises(Exception):
|
||||
async_ex = async_raises(Exception)
|
||||
await await_or_reraise(async_ex())
|
||||
|
||||
with pytest.raises(HomeAssistantError):
|
||||
async_ex = async_raises(AqualinkServiceException)
|
||||
await await_or_reraise(async_ex())
|
Loading…
Add table
Reference in a new issue