Update to iaqualink 0.4.1 (#53745)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Florent Thoumie 2021-12-27 12:20:55 -08:00 committed by GitHub
parent 5824477298
commit 3c2d5d5f8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 580 additions and 65 deletions

View file

@ -6,16 +6,16 @@ from functools import wraps
import logging import logging
import aiohttp.client_exceptions import aiohttp.client_exceptions
from iaqualink import ( from iaqualink.client import AqualinkClient
from iaqualink.device import (
AqualinkBinarySensor, AqualinkBinarySensor,
AqualinkClient,
AqualinkDevice, AqualinkDevice,
AqualinkLight, AqualinkLight,
AqualinkLoginException,
AqualinkSensor, AqualinkSensor,
AqualinkThermostat, AqualinkThermostat,
AqualinkToggle, AqualinkToggle,
) )
from iaqualink.exception import AqualinkServiceException
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -73,12 +73,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Aqualink component.""" """Set up the Aqualink component."""
conf = config.get(DOMAIN) conf = config.get(DOMAIN)
hass.data[DOMAIN] = {}
if conf is not None: if conf is not None:
hass.async_create_task( hass.async_create_task(
hass.config_entries.flow.async_init( 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] username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD] password = entry.data[CONF_PASSWORD]
hass.data.setdefault(DOMAIN, {})
# These will contain the initialized devices # These will contain the initialized devices
binary_sensors = hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] = [] binary_sensors = hass.data[DOMAIN][BINARY_SENSOR_DOMAIN] = []
climates = hass.data[DOMAIN][CLIMATE_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) aqualink = AqualinkClient(username, password, session)
try: try:
await aqualink.login() await aqualink.login()
except AqualinkLoginException as login_exception: except AqualinkServiceException as login_exception:
_LOGGER.error("Failed to login: %s", login_exception) _LOGGER.error("Failed to login: %s", login_exception)
return False return False
except ( except (
asyncio.TimeoutError, asyncio.TimeoutError,
aiohttp.client_exceptions.ClientConnectorError, aiohttp.client_exceptions.ClientConnectorError,
) as aio_exception: ) as aio_exception:
_LOGGER.warning("Exception raised while attempting to login: %s", aio_exception) raise ConfigEntryNotReady(
raise ConfigEntryNotReady from aio_exception 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()) systems = list(systems.values())
if not systems: if not systems:
_LOGGER.error("No systems detected or supported") _LOGGER.error("No systems detected or supported")
return False return False
# Only supporting the first system for now. # 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(): for dev in devices.values():
if isinstance(dev, AqualinkThermostat): if isinstance(dev, AqualinkThermostat):
@ -151,15 +165,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_systems_update(now): async def _async_systems_update(now):
"""Refresh internal state for all systems.""" """Refresh internal state for all systems."""
prev = systems[0].last_run_success prev = systems[0].online
await systems[0].update() try:
success = systems[0].last_run_success await systems[0].update()
except AqualinkServiceException as svc_exception:
if not success and prev: if prev is not None:
_LOGGER.warning("Failed to refresh iAqualink state") _LOGGER.warning("Failed to refresh iAqualink state: %s", svc_exception)
elif success and not prev: else:
_LOGGER.warning("Reconnected to iAqualink") cur = systems[0].online
if cur is True and prev is not True:
_LOGGER.warning("Reconnected to iAqualink")
async_dispatcher_send(hass, DOMAIN) 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] 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) return await hass.config_entries.async_unload_platforms(entry, platforms_to_unload)
@ -228,12 +244,12 @@ class AqualinkEntity(Entity):
@property @property
def assumed_state(self) -> bool: def assumed_state(self) -> bool:
"""Return whether the state is based on actual reading from the device.""" """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 @property
def available(self) -> bool: def available(self) -> bool:
"""Return whether the device is available or not.""" """Return whether the device is available or not."""
return self.dev.system.online return self.dev.system.online is True
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:

View file

@ -3,13 +3,13 @@ from __future__ import annotations
import logging import logging
from iaqualink import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState
from iaqualink.const import ( from iaqualink.const import (
AQUALINK_TEMP_CELSIUS_HIGH, AQUALINK_TEMP_CELSIUS_HIGH,
AQUALINK_TEMP_CELSIUS_LOW, AQUALINK_TEMP_CELSIUS_LOW,
AQUALINK_TEMP_FAHRENHEIT_HIGH, AQUALINK_TEMP_FAHRENHEIT_HIGH,
AQUALINK_TEMP_FAHRENHEIT_LOW, AQUALINK_TEMP_FAHRENHEIT_LOW,
) )
from iaqualink.device import AqualinkHeater, AqualinkPump, AqualinkSensor, AqualinkState
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
@ -24,6 +24,7 @@ from homeassistant.core import HomeAssistant
from . import AqualinkEntity, refresh_system from . import AqualinkEntity, refresh_system
from .const import CLIMATE_SUPPORTED_MODES, DOMAIN as AQUALINK_DOMAIN from .const import CLIMATE_SUPPORTED_MODES, DOMAIN as AQUALINK_DOMAIN
from .utils import await_or_reraise
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -76,9 +77,9 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: str) -> None: async def async_set_hvac_mode(self, hvac_mode: str) -> None:
"""Turn the underlying heater switch on or off.""" """Turn the underlying heater switch on or off."""
if hvac_mode == HVAC_MODE_HEAT: 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: elif hvac_mode == HVAC_MODE_OFF:
await self.heater.turn_off() await await_or_reraise(self.heater.turn_off())
else: else:
_LOGGER.warning("Unknown operation mode: %s", hvac_mode) _LOGGER.warning("Unknown operation mode: %s", hvac_mode)
@ -111,7 +112,7 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
@refresh_system @refresh_system
async def async_set_temperature(self, **kwargs) -> None: async def async_set_temperature(self, **kwargs) -> None:
"""Set new target temperature.""" """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 @property
def sensor(self) -> AqualinkSensor: def sensor(self) -> AqualinkSensor:

View file

@ -3,7 +3,11 @@ from __future__ import annotations
from typing import Any from typing import Any
from iaqualink import AqualinkClient, AqualinkLoginException from iaqualink.client import AqualinkClient
from iaqualink.exception import (
AqualinkServiceException,
AqualinkServiceUnauthorizedException,
)
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -32,16 +36,22 @@ class AqualinkFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
password = user_input[CONF_PASSWORD] password = user_input[CONF_PASSWORD]
try: try:
aqualink = AqualinkClient(username, password) async with AqualinkClient(username, password):
await aqualink.login() pass
return self.async_create_entry(title=username, data=user_input) except AqualinkServiceUnauthorizedException:
except AqualinkLoginException: errors["base"] = "invalid_auth"
except AqualinkServiceException:
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
else:
return self.async_create_entry(title=username, data=user_input)
return self.async_show_form( return self.async_show_form(
step_id="user", step_id="user",
data_schema=vol.Schema( 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, errors=errors,
) )

View file

@ -1,5 +1,5 @@
"""Support for Aqualink pool lights.""" """Support for Aqualink pool lights."""
from iaqualink import AqualinkLightEffect import logging
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -14,6 +14,9 @@ from homeassistant.core import HomeAssistant
from . import AqualinkEntity, refresh_system from . import AqualinkEntity, refresh_system
from .const import DOMAIN as AQUALINK_DOMAIN from .const import DOMAIN as AQUALINK_DOMAIN
from .utils import await_or_reraise
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -49,20 +52,19 @@ class HassAqualinkLight(AqualinkEntity, LightEntity):
them. them.
""" """
# For now I'm assuming lights support either effects or brightness. # For now I'm assuming lights support either effects or brightness.
if effect := kwargs.get(ATTR_EFFECT): if effect_name := kwargs.get(ATTR_EFFECT):
effect = AqualinkLightEffect[effect].value await await_or_reraise(self.dev.set_effect_by_name(effect_name))
await self.dev.set_effect(effect)
elif brightness := kwargs.get(ATTR_BRIGHTNESS): elif brightness := kwargs.get(ATTR_BRIGHTNESS):
# Aqualink supports percentages in 25% increments. # Aqualink supports percentages in 25% increments.
pct = int(round(brightness * 4.0 / 255)) * 25 pct = int(round(brightness * 4.0 / 255)) * 25
await self.dev.set_brightness(pct) await await_or_reraise(self.dev.set_brightness(pct))
else: else:
await self.dev.turn_on() await await_or_reraise(self.dev.turn_on())
@refresh_system @refresh_system
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn off the light.""" """Turn off the light."""
await self.dev.turn_off() await await_or_reraise(self.dev.turn_off())
@property @property
def brightness(self) -> int: def brightness(self) -> int:
@ -75,12 +77,12 @@ class HassAqualinkLight(AqualinkEntity, LightEntity):
@property @property
def effect(self) -> str: def effect(self) -> str:
"""Return the current light effect if supported.""" """Return the current light effect if supported."""
return AqualinkLightEffect(self.dev.effect).name return self.dev.effect
@property @property
def effect_list(self) -> list: def effect_list(self) -> list:
"""Return supported light effects.""" """Return supported light effects."""
return list(AqualinkLightEffect.__members__) return list(self.dev.supported_light_effects)
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:

View file

@ -4,6 +4,6 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/iaqualink/", "documentation": "https://www.home-assistant.io/integrations/iaqualink/",
"codeowners": ["@flz"], "codeowners": ["@flz"],
"requirements": ["iaqualink==0.3.90"], "requirements": ["iaqualink==0.4.1"],
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
} }

View file

@ -11,7 +11,8 @@
} }
}, },
"error": { "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": { "abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]" "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"

View file

@ -5,6 +5,7 @@ from homeassistant.core import HomeAssistant
from . import AqualinkEntity, refresh_system from . import AqualinkEntity, refresh_system
from .const import DOMAIN as AQUALINK_DOMAIN from .const import DOMAIN as AQUALINK_DOMAIN
from .utils import await_or_reraise
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@ -47,9 +48,9 @@ class HassAqualinkSwitch(AqualinkEntity, SwitchEntity):
@refresh_system @refresh_system
async def async_turn_on(self, **kwargs) -> None: async def async_turn_on(self, **kwargs) -> None:
"""Turn on the switch.""" """Turn on the switch."""
await self.dev.turn_on() await await_or_reraise(self.dev.turn_on())
@refresh_system @refresh_system
async def async_turn_off(self, **kwargs) -> None: async def async_turn_off(self, **kwargs) -> None:
"""Turn off the switch.""" """Turn off the switch."""
await self.dev.turn_off() await await_or_reraise(self.dev.turn_off())

View 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

View file

@ -869,7 +869,7 @@ hyperion-py==0.7.4
iammeter==0.1.7 iammeter==0.1.7
# homeassistant.components.iaqualink # homeassistant.components.iaqualink
iaqualink==0.3.90 iaqualink==0.4.1
# homeassistant.components.watson_tts # homeassistant.components.watson_tts
ibm-watson==5.2.2 ibm-watson==5.2.2

View file

@ -550,7 +550,7 @@ huisbaasje-client==0.1.0
hyperion-py==0.7.4 hyperion-py==0.7.4
# homeassistant.components.iaqualink # homeassistant.components.iaqualink
iaqualink==0.3.90 iaqualink==0.4.1
# homeassistant.components.ping # homeassistant.components.ping
icmplib==3.0 icmplib==3.0

View 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,
)

View file

@ -1,20 +1,19 @@
"""Tests for iAqualink config flow.""" """Tests for iAqualink config flow."""
from unittest.mock import patch from unittest.mock import patch
import iaqualink from iaqualink.exception import (
AqualinkServiceException,
AqualinkServiceUnauthorizedException,
)
import pytest import pytest
from homeassistant.components.iaqualink import config_flow 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"]) @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.""" """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 = config_flow.AqualinkFlowHandler()
flow.hass = hass flow.hass = hass
@ -22,14 +21,14 @@ async def test_already_configured(hass, step):
fname = f"async_step_{step}" fname = f"async_step_{step}"
func = getattr(flow, fname) func = getattr(flow, fname)
result = await func(DATA) result = await func(config_data)
assert result["type"] == "abort" assert result["type"] == "abort"
@pytest.mark.parametrize("step", ["import", "user"]) @pytest.mark.parametrize("step", ["import", "user"])
async def test_without_config(hass, step): async def test_without_config(hass, step):
"""Test with no configuration.""" """Test config flow with no configuration."""
flow = config_flow.AqualinkFlowHandler() flow = config_flow.AqualinkFlowHandler()
flow.hass = hass flow.hass = hass
flow.context = {} flow.context = {}
@ -44,7 +43,7 @@ async def test_without_config(hass, step):
@pytest.mark.parametrize("step", ["import", "user"]) @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.""" """Test config flow with invalid username and/or password."""
flow = config_flow.AqualinkFlowHandler() flow = config_flow.AqualinkFlowHandler()
flow.hass = hass flow.hass = hass
@ -52,9 +51,29 @@ async def test_with_invalid_credentials(hass, step):
fname = f"async_step_{step}" fname = f"async_step_{step}"
func = getattr(flow, fname) func = getattr(flow, fname)
with patch( 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["type"] == "form"
assert result["step_id"] == "user" assert result["step_id"] == "user"
@ -62,17 +81,20 @@ async def test_with_invalid_credentials(hass, step):
@pytest.mark.parametrize("step", ["import", "user"]) @pytest.mark.parametrize("step", ["import", "user"])
async def test_with_existing_config(hass, step): async def test_with_existing_config(hass, config_data, step):
"""Test with existing configuration.""" """Test config flow with existing configuration."""
flow = config_flow.AqualinkFlowHandler() flow = config_flow.AqualinkFlowHandler()
flow.hass = hass flow.hass = hass
flow.context = {} flow.context = {}
fname = f"async_step_{step}" fname = f"async_step_{step}"
func = getattr(flow, fname) func = getattr(flow, fname)
with patch("iaqualink.AqualinkClient.login", return_value=mock_coro(None)): with patch(
result = await func(DATA) "homeassistant.components.iaqualink.config_flow.AqualinkClient.login",
return_value=None,
):
result = await func(config_data)
assert result["type"] == "create_entry" assert result["type"] == "create_entry"
assert result["title"] == DATA["username"] assert result["title"] == config_data["username"]
assert result["data"] == DATA assert result["data"] == config_data

View 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

View 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())