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 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:

View File

@ -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:

View File

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

View File

@ -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:

View File

@ -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"
}

View File

@ -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%]"

View File

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

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
# homeassistant.components.iaqualink
iaqualink==0.3.90
iaqualink==0.4.1
# homeassistant.components.watson_tts
ibm-watson==5.2.2

View File

@ -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

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."""
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

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