Update to iaqualink 0.5.0 (#80304)

* Update to iaqualink 0.5.0.

* Boolean conditional style fix

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fix black formatting

* Update iaqualink tests after update to 0.5.x

* Remove debug print statements

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Florent Thoumie 2022-10-17 01:14:29 -07:00 committed by GitHub
parent bbb0b2a0e1
commit abec592a24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 107 additions and 110 deletions

View File

@ -14,8 +14,8 @@ from iaqualink.device import (
AqualinkDevice, AqualinkDevice,
AqualinkLight, AqualinkLight,
AqualinkSensor, AqualinkSensor,
AqualinkSwitch,
AqualinkThermostat, AqualinkThermostat,
AqualinkToggle,
) )
from iaqualink.exception import AqualinkServiceException from iaqualink.exception import AqualinkServiceException
from typing_extensions import Concatenate, ParamSpec from typing_extensions import Concatenate, ParamSpec
@ -29,7 +29,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_connect,
async_dispatcher_send, async_dispatcher_send,
@ -56,7 +55,9 @@ PLATFORMS = [
] ]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry( # noqa: C901
hass: HomeAssistant, entry: ConfigEntry
) -> bool:
"""Set up Aqualink from a config entry.""" """Set up Aqualink from a config entry."""
username = entry.data[CONF_USERNAME] username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD] password = entry.data[CONF_PASSWORD]
@ -70,17 +71,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = [] sensors = hass.data[DOMAIN][SENSOR_DOMAIN] = []
switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] switches = hass.data[DOMAIN][SWITCH_DOMAIN] = []
session = async_get_clientsession(hass) aqualink = AqualinkClient(username, password)
aqualink = AqualinkClient(username, password, session)
try: try:
await aqualink.login() await aqualink.login()
except AqualinkServiceException as login_exception: except AqualinkServiceException as login_exception:
_LOGGER.error("Failed to login: %s", login_exception) _LOGGER.error("Failed to login: %s", login_exception)
await aqualink.close()
return False return False
except ( except (
asyncio.TimeoutError, asyncio.TimeoutError,
aiohttp.client_exceptions.ClientConnectorError, aiohttp.client_exceptions.ClientConnectorError,
) as aio_exception: ) as aio_exception:
await aqualink.close()
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
f"Error while attempting login: {aio_exception}" f"Error while attempting login: {aio_exception}"
) from aio_exception ) from aio_exception
@ -88,6 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
systems = await aqualink.get_systems() systems = await aqualink.get_systems()
except AqualinkServiceException as svc_exception: except AqualinkServiceException as svc_exception:
await aqualink.close()
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
f"Error while attempting to retrieve systems list: {svc_exception}" f"Error while attempting to retrieve systems list: {svc_exception}"
) from svc_exception ) from svc_exception
@ -95,27 +98,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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")
await aqualink.close()
return False return False
# Only supporting the first system for now. for system in systems:
try: try:
devices = await systems[0].get_devices() devices = await system.get_devices()
except AqualinkServiceException as svc_exception: except AqualinkServiceException as svc_exception:
raise ConfigEntryNotReady( await aqualink.close()
f"Error while attempting to retrieve devices list: {svc_exception}" raise ConfigEntryNotReady(
) from svc_exception 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):
climates += [dev] climates += [dev]
elif isinstance(dev, AqualinkLight): elif isinstance(dev, AqualinkLight):
lights += [dev] lights += [dev]
elif isinstance(dev, AqualinkBinarySensor): elif isinstance(dev, AqualinkSwitch):
binary_sensors += [dev] switches += [dev]
elif isinstance(dev, AqualinkSensor): elif isinstance(dev, AqualinkBinarySensor):
sensors += [dev] binary_sensors += [dev]
elif isinstance(dev, AqualinkToggle): elif isinstance(dev, AqualinkSensor):
switches += [dev] sensors += [dev]
platforms = [] platforms = []
if binary_sensors: if binary_sensors:
@ -134,23 +139,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug("Got %s switches: %s", len(switches), switches) _LOGGER.debug("Got %s switches: %s", len(switches), switches)
platforms.append(Platform.SWITCH) platforms.append(Platform.SWITCH)
hass.data[DOMAIN]["client"] = aqualink
await hass.config_entries.async_forward_entry_setups(entry, platforms) await hass.config_entries.async_forward_entry_setups(entry, platforms)
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].online for system in systems:
prev = system.online
try: try:
await systems[0].update() await system.update()
except AqualinkServiceException as svc_exception: except AqualinkServiceException as svc_exception:
if prev is not None: if prev is not None:
_LOGGER.warning("Failed to refresh iAqualink state: %s", svc_exception) _LOGGER.warning(
else: "Failed to refresh system %s state: %s",
cur = systems[0].online system.serial,
if cur is True and prev is not True: svc_exception,
_LOGGER.warning("Reconnected to iAqualink") )
else:
cur = system.online
if cur and not prev:
_LOGGER.warning("System %s reconnected to iAqualink", system.serial)
async_dispatcher_send(hass, DOMAIN) async_dispatcher_send(hass, DOMAIN)
async_track_time_interval(hass, _async_systems_update, UPDATE_INTERVAL) async_track_time_interval(hass, _async_systems_update, UPDATE_INTERVAL)
@ -159,6 +171,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
aqualink = hass.data[DOMAIN]["client"]
await aqualink.close()
platforms_to_unload = [ platforms_to_unload = [
platform for platform in PLATFORMS if platform in hass.data[DOMAIN] platform for platform in PLATFORMS if platform in hass.data[DOMAIN]
] ]
@ -226,8 +241,8 @@ class AqualinkEntity(Entity):
"""Return the device info.""" """Return the device info."""
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)}, identifiers={(DOMAIN, self.unique_id)},
manufacturer="Jandy", manufacturer=self.dev.manufacturer,
model=self.dev.__class__.__name__.replace("Aqualink", ""), model=self.dev.model,
name=self.name, name=self.name,
via_device=(DOMAIN, self.dev.system.serial), via_device=(DOMAIN, self.dev.system.serial),
) )

View File

@ -4,14 +4,6 @@ from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
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 ( from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN, DOMAIN as CLIMATE_DOMAIN,
ClimateEntity, ClimateEntity,
@ -55,17 +47,10 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
"""Return the name of the thermostat.""" """Return the name of the thermostat."""
return self.dev.label.split(" ")[0] return self.dev.label.split(" ")[0]
@property
def pump(self) -> AqualinkPump:
"""Return the pump device for the current thermostat."""
pump = f"{self.name.lower()}_pump"
return self.dev.system.devices[pump]
@property @property
def hvac_mode(self) -> HVACMode: def hvac_mode(self) -> HVACMode:
"""Return the current HVAC mode.""" """Return the current HVAC mode."""
state = AqualinkState(self.heater.state) if self.dev.is_on is True:
if state == AqualinkState.ON:
return HVACMode.HEAT return HVACMode.HEAT
return HVACMode.OFF return HVACMode.OFF
@ -73,32 +58,28 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Turn the underlying heater switch on or off.""" """Turn the underlying heater switch on or off."""
if hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.HEAT:
await await_or_reraise(self.heater.turn_on()) await await_or_reraise(self.dev.turn_on())
elif hvac_mode == HVACMode.OFF: elif hvac_mode == HVACMode.OFF:
await await_or_reraise(self.heater.turn_off()) await await_or_reraise(self.dev.turn_off())
else: else:
_LOGGER.warning("Unknown operation mode: %s", hvac_mode) _LOGGER.warning("Unknown operation mode: %s", hvac_mode)
@property @property
def temperature_unit(self) -> str: def temperature_unit(self) -> str:
"""Return the unit of measurement.""" """Return the unit of measurement."""
if self.dev.system.temp_unit == "F": if self.dev.unit == "F":
return TEMP_FAHRENHEIT return TEMP_FAHRENHEIT
return TEMP_CELSIUS return TEMP_CELSIUS
@property @property
def min_temp(self) -> int: def min_temp(self) -> int:
"""Return the minimum temperature supported by the thermostat.""" """Return the minimum temperature supported by the thermostat."""
if self.temperature_unit == TEMP_FAHRENHEIT: return self.dev.min_temperature
return AQUALINK_TEMP_FAHRENHEIT_LOW
return AQUALINK_TEMP_CELSIUS_LOW
@property @property
def max_temp(self) -> int: def max_temp(self) -> int:
"""Return the minimum temperature supported by the thermostat.""" """Return the minimum temperature supported by the thermostat."""
if self.temperature_unit == TEMP_FAHRENHEIT: return self.dev.max_temperature
return AQUALINK_TEMP_FAHRENHEIT_HIGH
return AQUALINK_TEMP_CELSIUS_HIGH
@property @property
def target_temperature(self) -> float: def target_temperature(self) -> float:
@ -110,21 +91,9 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity):
"""Set new target temperature.""" """Set new target temperature."""
await await_or_reraise(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:
"""Return the sensor device for the current thermostat."""
sensor = f"{self.name.lower()}_temp"
return self.dev.system.devices[sensor]
@property @property
def current_temperature(self) -> float | None: def current_temperature(self) -> float | None:
"""Return the current temperature.""" """Return the current temperature."""
if self.sensor.state != "": if self.dev.current_temperature != "":
return float(self.sensor.state) return float(self.dev.current_temperature)
return None return None
@property
def heater(self) -> AqualinkHeater:
"""Return the heater device for the current thermostat."""
heater = f"{self.name.lower()}_heater"
return self.dev.system.devices[heater]

View File

@ -2,4 +2,4 @@
from datetime import timedelta from datetime import timedelta
DOMAIN = "iaqualink" DOMAIN = "iaqualink"
UPDATE_INTERVAL = timedelta(seconds=30) UPDATE_INTERVAL = timedelta(seconds=15)

View File

@ -90,7 +90,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity):
@property @property
def color_mode(self) -> ColorMode: def color_mode(self) -> ColorMode:
"""Return the color mode of the light.""" """Return the color mode of the light."""
if self.dev.is_dimmer: if self.dev.supports_brightness:
return ColorMode.BRIGHTNESS return ColorMode.BRIGHTNESS
return ColorMode.ONOFF return ColorMode.ONOFF
@ -102,7 +102,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity):
@property @property
def supported_features(self) -> int: def supported_features(self) -> int:
"""Return the list of features supported by the light.""" """Return the list of features supported by the light."""
if self.dev.is_color: if self.dev.supports_effect:
return LightEntityFeature.EFFECT return LightEntityFeature.EFFECT
return 0 return 0

View File

@ -4,7 +4,7 @@
"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.4.1"], "requirements": ["iaqualink==0.5.0"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["iaqualink"] "loggers": ["iaqualink"]
} }

View File

@ -901,7 +901,7 @@ hyperion-py==0.7.5
iammeter==0.1.7 iammeter==0.1.7
# homeassistant.components.iaqualink # homeassistant.components.iaqualink
iaqualink==0.4.1 iaqualink==0.5.0
# homeassistant.components.ibeacon # homeassistant.components.ibeacon
ibeacon_ble==0.7.4 ibeacon_ble==0.7.4

View File

@ -672,7 +672,7 @@ huawei-lte-api==1.6.3
hyperion-py==0.7.5 hyperion-py==0.7.5
# homeassistant.components.iaqualink # homeassistant.components.iaqualink
iaqualink==0.4.1 iaqualink==0.5.0
# homeassistant.components.ibeacon # homeassistant.components.ibeacon
ibeacon_ble==0.7.4 ibeacon_ble==0.7.4

View File

@ -1,6 +1,6 @@
"""Configuration for iAqualink tests.""" """Configuration for iAqualink tests."""
import random import random
from unittest.mock import AsyncMock from unittest.mock import AsyncMock, PropertyMock, patch
from iaqualink.client import AqualinkClient from iaqualink.client import AqualinkClient
from iaqualink.device import AqualinkDevice from iaqualink.device import AqualinkDevice
@ -47,14 +47,31 @@ def get_aqualink_system(aqualink, cls=None, data=None):
return cls(aqualink=aqualink, data=data) return cls(aqualink=aqualink, data=data)
def get_aqualink_device(system, cls=None, data=None): def get_aqualink_device(system, name, cls=None, data=None):
"""Create aqualink device.""" """Create aqualink device."""
if cls is None: if cls is None:
cls = AqualinkDevice cls = AqualinkDevice
# AqualinkDevice doesn't implement some of the properties since it's left to
# sub-classes for them to do. Provide a basic implementation here for the
# benefits of the test suite.
attrs = {
"name": name,
"manufacturer": "Jandy",
"model": "Device",
"label": name.upper(),
}
for k, v in attrs.items():
patcher = patch.object(cls, k, new_callable=PropertyMock)
mock = patcher.start()
mock.return_value = v
if data is None: if data is None:
data = {} data = {}
data["name"] = name
return cls(system=system, data=data) return cls(system=system, data=data)
@ -72,7 +89,7 @@ def config_fixture():
@pytest.fixture(name="config_entry") @pytest.fixture(name="config_entry")
def config_entry_fixture(): def config_entry_fixture():
"""Create a mock HEOS config entry.""" """Create a mock config entry."""
return MockConfigEntry( return MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
data=MOCK_DATA, data=MOCK_DATA,

View File

@ -4,15 +4,15 @@ import asyncio
import logging import logging
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from iaqualink.device import (
AqualinkAuxToggle,
AqualinkBinarySensor,
AqualinkDevice,
AqualinkLightToggle,
AqualinkSensor,
AqualinkThermostat,
)
from iaqualink.exception import AqualinkServiceException from iaqualink.exception import AqualinkServiceException
from iaqualink.systems.iaqua.device import (
IaquaAuxSwitch,
IaquaBinarySensor,
IaquaLightSwitch,
IaquaSensor,
IaquaThermostat,
)
from iaqualink.systems.iaqua.system import IaquaSystem
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
@ -101,7 +101,7 @@ async def test_setup_devices_exception(hass, config_entry, client):
"""Test setup encountering an exception while retrieving devices.""" """Test setup encountering an exception while retrieving devices."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
system = get_aqualink_system(client) system = get_aqualink_system(client, cls=IaquaSystem)
systems = {system.serial: system} systems = {system.serial: system}
with patch( with patch(
@ -124,10 +124,10 @@ async def test_setup_all_good_no_recognized_devices(hass, config_entry, client):
"""Test setup ending in no devices recognized.""" """Test setup ending in no devices recognized."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
system = get_aqualink_system(client) system = get_aqualink_system(client, cls=IaquaSystem)
systems = {system.serial: system} systems = {system.serial: system}
device = get_aqualink_device(system, AqualinkDevice, data={"name": "dev_1"}) device = get_aqualink_device(system, name="dev_1")
devices = {device.name: device} devices = {device.name: device}
with patch( with patch(
@ -161,19 +161,15 @@ async def test_setup_all_good_all_device_types(hass, config_entry, client):
"""Test setup ending in one device of each type recognized.""" """Test setup ending in one device of each type recognized."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
system = get_aqualink_system(client) system = get_aqualink_system(client, cls=IaquaSystem)
systems = {system.serial: system} systems = {system.serial: system}
devices = [ devices = [
get_aqualink_device(system, AqualinkAuxToggle, data={"name": "aux_1"}), get_aqualink_device(system, name="aux_1", cls=IaquaAuxSwitch),
get_aqualink_device( get_aqualink_device(system, name="freeze_protection", cls=IaquaBinarySensor),
system, AqualinkBinarySensor, data={"name": "freeze_protection"} get_aqualink_device(system, name="aux_2", cls=IaquaLightSwitch),
), get_aqualink_device(system, name="ph", cls=IaquaSensor),
get_aqualink_device(system, AqualinkLightToggle, data={"name": "aux_2"}), get_aqualink_device(system, name="pool_set_point", cls=IaquaThermostat),
get_aqualink_device(system, AqualinkSensor, data={"name": "ph"}),
get_aqualink_device(
system, AqualinkThermostat, data={"name": "pool_set_point"}
),
] ]
devices = {d.name: d for d in devices} devices = {d.name: d for d in devices}
@ -207,7 +203,7 @@ async def test_multiple_updates(hass, config_entry, caplog, client):
"""Test all possible results of online status transition after update.""" """Test all possible results of online status transition after update."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
system = get_aqualink_system(client) system = get_aqualink_system(client, cls=IaquaSystem)
systems = {system.serial: system} systems = {system.serial: system}
system.get_devices = AsyncMock(return_value={}) system.get_devices = AsyncMock(return_value={})
@ -269,7 +265,7 @@ async def test_multiple_updates(hass, config_entry, caplog, client):
system.update.side_effect = set_online_to_true system.update.side_effect = set_online_to_true
await _ffwd_next_update_interval(hass) await _ffwd_next_update_interval(hass)
assert len(caplog.records) == 1 assert len(caplog.records) == 1
assert "Reconnected" in caplog.text assert "reconnected" in caplog.text
# False -> None / ServiceException # False -> None / ServiceException
system.online = False system.online = False
@ -292,7 +288,7 @@ async def test_multiple_updates(hass, config_entry, caplog, client):
system.update.side_effect = set_online_to_true system.update.side_effect = set_online_to_true
await _ffwd_next_update_interval(hass) await _ffwd_next_update_interval(hass)
assert len(caplog.records) == 1 assert len(caplog.records) == 1
assert "Reconnected" in caplog.text assert "reconnected" in caplog.text
# None -> False # None -> False
system.online = None system.online = None
@ -311,11 +307,11 @@ async def test_entity_assumed_and_available(hass, config_entry, client):
"""Test assumed_state and_available properties for all values of online.""" """Test assumed_state and_available properties for all values of online."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
system = get_aqualink_system(client) system = get_aqualink_system(client, cls=IaquaSystem)
systems = {system.serial: system} systems = {system.serial: system}
light = get_aqualink_device( light = get_aqualink_device(
system, AqualinkLightToggle, data={"name": "aux_1", "state": "1"} system, name="aux_1", cls=IaquaLightSwitch, data={"state": "1"}
) )
devices = {d.name: d for d in [light]} devices = {d.name: d for d in [light]}
system.get_devices = AsyncMock(return_value=devices) system.get_devices = AsyncMock(return_value=devices)