diff --git a/homeassistant/components/iaqualink/__init__.py b/homeassistant/components/iaqualink/__init__.py index 61f27e8970c..1bd9d7d72cf 100644 --- a/homeassistant/components/iaqualink/__init__.py +++ b/homeassistant/components/iaqualink/__init__.py @@ -14,8 +14,8 @@ from iaqualink.device import ( AqualinkDevice, AqualinkLight, AqualinkSensor, + AqualinkSwitch, AqualinkThermostat, - AqualinkToggle, ) from iaqualink.exception import AqualinkServiceException 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.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, 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.""" username = entry.data[CONF_USERNAME] 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] = [] switches = hass.data[DOMAIN][SWITCH_DOMAIN] = [] - session = async_get_clientsession(hass) - aqualink = AqualinkClient(username, password, session) + aqualink = AqualinkClient(username, password) try: await aqualink.login() except AqualinkServiceException as login_exception: _LOGGER.error("Failed to login: %s", login_exception) + await aqualink.close() return False except ( asyncio.TimeoutError, aiohttp.client_exceptions.ClientConnectorError, ) as aio_exception: + await aqualink.close() raise ConfigEntryNotReady( f"Error while attempting login: {aio_exception}" ) from aio_exception @@ -88,6 +90,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: systems = await aqualink.get_systems() except AqualinkServiceException as svc_exception: + await aqualink.close() raise ConfigEntryNotReady( f"Error while attempting to retrieve systems list: {svc_exception}" ) from svc_exception @@ -95,27 +98,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: systems = list(systems.values()) if not systems: _LOGGER.error("No systems detected or supported") + await aqualink.close() return False - # Only supporting the first system for now. - 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 system in systems: + try: + devices = await system.get_devices() + except AqualinkServiceException as svc_exception: + await aqualink.close() + raise ConfigEntryNotReady( + f"Error while attempting to retrieve devices list: {svc_exception}" + ) from svc_exception - for dev in devices.values(): - if isinstance(dev, AqualinkThermostat): - climates += [dev] - elif isinstance(dev, AqualinkLight): - lights += [dev] - elif isinstance(dev, AqualinkBinarySensor): - binary_sensors += [dev] - elif isinstance(dev, AqualinkSensor): - sensors += [dev] - elif isinstance(dev, AqualinkToggle): - switches += [dev] + for dev in devices.values(): + if isinstance(dev, AqualinkThermostat): + climates += [dev] + elif isinstance(dev, AqualinkLight): + lights += [dev] + elif isinstance(dev, AqualinkSwitch): + switches += [dev] + elif isinstance(dev, AqualinkBinarySensor): + binary_sensors += [dev] + elif isinstance(dev, AqualinkSensor): + sensors += [dev] platforms = [] 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) platforms.append(Platform.SWITCH) + hass.data[DOMAIN]["client"] = aqualink + await hass.config_entries.async_forward_entry_setups(entry, platforms) async def _async_systems_update(now): """Refresh internal state for all systems.""" - prev = systems[0].online + for system in systems: + prev = system.online - 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") + try: + await system.update() + except AqualinkServiceException as svc_exception: + if prev is not None: + _LOGGER.warning( + "Failed to refresh system %s state: %s", + system.serial, + svc_exception, + ) + 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) @@ -159,6 +171,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" + aqualink = hass.data[DOMAIN]["client"] + await aqualink.close() + platforms_to_unload = [ platform for platform in PLATFORMS if platform in hass.data[DOMAIN] ] @@ -226,8 +241,8 @@ class AqualinkEntity(Entity): """Return the device info.""" return DeviceInfo( identifiers={(DOMAIN, self.unique_id)}, - manufacturer="Jandy", - model=self.dev.__class__.__name__.replace("Aqualink", ""), + manufacturer=self.dev.manufacturer, + model=self.dev.model, name=self.name, via_device=(DOMAIN, self.dev.system.serial), ) diff --git a/homeassistant/components/iaqualink/climate.py b/homeassistant/components/iaqualink/climate.py index 725f1b9084e..408bd56778e 100644 --- a/homeassistant/components/iaqualink/climate.py +++ b/homeassistant/components/iaqualink/climate.py @@ -4,14 +4,6 @@ from __future__ import annotations import logging 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 ( DOMAIN as CLIMATE_DOMAIN, ClimateEntity, @@ -55,17 +47,10 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): """Return the name of the thermostat.""" 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 def hvac_mode(self) -> HVACMode: """Return the current HVAC mode.""" - state = AqualinkState(self.heater.state) - if state == AqualinkState.ON: + if self.dev.is_on is True: return HVACMode.HEAT return HVACMode.OFF @@ -73,32 +58,28 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Turn the underlying heater switch on or off.""" if hvac_mode == HVACMode.HEAT: - await await_or_reraise(self.heater.turn_on()) + await await_or_reraise(self.dev.turn_on()) elif hvac_mode == HVACMode.OFF: - await await_or_reraise(self.heater.turn_off()) + await await_or_reraise(self.dev.turn_off()) else: _LOGGER.warning("Unknown operation mode: %s", hvac_mode) @property def temperature_unit(self) -> str: """Return the unit of measurement.""" - if self.dev.system.temp_unit == "F": + if self.dev.unit == "F": return TEMP_FAHRENHEIT return TEMP_CELSIUS @property def min_temp(self) -> int: """Return the minimum temperature supported by the thermostat.""" - if self.temperature_unit == TEMP_FAHRENHEIT: - return AQUALINK_TEMP_FAHRENHEIT_LOW - return AQUALINK_TEMP_CELSIUS_LOW + return self.dev.min_temperature @property def max_temp(self) -> int: """Return the minimum temperature supported by the thermostat.""" - if self.temperature_unit == TEMP_FAHRENHEIT: - return AQUALINK_TEMP_FAHRENHEIT_HIGH - return AQUALINK_TEMP_CELSIUS_HIGH + return self.dev.max_temperature @property def target_temperature(self) -> float: @@ -110,21 +91,9 @@ class HassAqualinkThermostat(AqualinkEntity, ClimateEntity): """Set new target 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 def current_temperature(self) -> float | None: """Return the current temperature.""" - if self.sensor.state != "": - return float(self.sensor.state) + if self.dev.current_temperature != "": + return float(self.dev.current_temperature) 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] diff --git a/homeassistant/components/iaqualink/const.py b/homeassistant/components/iaqualink/const.py index 189d7083b2d..7cabfa2b4f6 100644 --- a/homeassistant/components/iaqualink/const.py +++ b/homeassistant/components/iaqualink/const.py @@ -2,4 +2,4 @@ from datetime import timedelta DOMAIN = "iaqualink" -UPDATE_INTERVAL = timedelta(seconds=30) +UPDATE_INTERVAL = timedelta(seconds=15) diff --git a/homeassistant/components/iaqualink/light.py b/homeassistant/components/iaqualink/light.py index 3a8b1e0fb4a..91ca64a87e6 100644 --- a/homeassistant/components/iaqualink/light.py +++ b/homeassistant/components/iaqualink/light.py @@ -90,7 +90,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): @property def color_mode(self) -> ColorMode: """Return the color mode of the light.""" - if self.dev.is_dimmer: + if self.dev.supports_brightness: return ColorMode.BRIGHTNESS return ColorMode.ONOFF @@ -102,7 +102,7 @@ class HassAqualinkLight(AqualinkEntity, LightEntity): @property def supported_features(self) -> int: """Return the list of features supported by the light.""" - if self.dev.is_color: + if self.dev.supports_effect: return LightEntityFeature.EFFECT return 0 diff --git a/homeassistant/components/iaqualink/manifest.json b/homeassistant/components/iaqualink/manifest.json index 7c57744fd3b..d5b7d7de0d8 100644 --- a/homeassistant/components/iaqualink/manifest.json +++ b/homeassistant/components/iaqualink/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iaqualink/", "codeowners": ["@flz"], - "requirements": ["iaqualink==0.4.1"], + "requirements": ["iaqualink==0.5.0"], "iot_class": "cloud_polling", "loggers": ["iaqualink"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2013c7c7c54..816621b2bf0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -901,7 +901,7 @@ hyperion-py==0.7.5 iammeter==0.1.7 # homeassistant.components.iaqualink -iaqualink==0.4.1 +iaqualink==0.5.0 # homeassistant.components.ibeacon ibeacon_ble==0.7.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fe4bb98109..f8cdd47286f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -672,7 +672,7 @@ huawei-lte-api==1.6.3 hyperion-py==0.7.5 # homeassistant.components.iaqualink -iaqualink==0.4.1 +iaqualink==0.5.0 # homeassistant.components.ibeacon ibeacon_ble==0.7.4 diff --git a/tests/components/iaqualink/conftest.py b/tests/components/iaqualink/conftest.py index 6a46e063501..b4db99dbe40 100644 --- a/tests/components/iaqualink/conftest.py +++ b/tests/components/iaqualink/conftest.py @@ -1,6 +1,6 @@ """Configuration for iAqualink tests.""" import random -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, PropertyMock, patch from iaqualink.client import AqualinkClient from iaqualink.device import AqualinkDevice @@ -47,14 +47,31 @@ def get_aqualink_system(aqualink, cls=None, data=None): 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.""" if cls is None: 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: data = {} + data["name"] = name + return cls(system=system, data=data) @@ -72,7 +89,7 @@ def config_fixture(): @pytest.fixture(name="config_entry") def config_entry_fixture(): - """Create a mock HEOS config entry.""" + """Create a mock config entry.""" return MockConfigEntry( domain=DOMAIN, data=MOCK_DATA, diff --git a/tests/components/iaqualink/test_init.py b/tests/components/iaqualink/test_init.py index bd2e072d213..3f2b822da81 100644 --- a/tests/components/iaqualink/test_init.py +++ b/tests/components/iaqualink/test_init.py @@ -4,15 +4,15 @@ 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 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.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.""" config_entry.add_to_hass(hass) - system = get_aqualink_system(client) + system = get_aqualink_system(client, cls=IaquaSystem) systems = {system.serial: system} 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.""" config_entry.add_to_hass(hass) - system = get_aqualink_system(client) + system = get_aqualink_system(client, cls=IaquaSystem) 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} 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.""" config_entry.add_to_hass(hass) - system = get_aqualink_system(client) + system = get_aqualink_system(client, cls=IaquaSystem) systems = {system.serial: system} devices = [ - get_aqualink_device(system, AqualinkAuxToggle, data={"name": "aux_1"}), - get_aqualink_device( - system, AqualinkBinarySensor, data={"name": "freeze_protection"} - ), - get_aqualink_device(system, AqualinkLightToggle, data={"name": "aux_2"}), - get_aqualink_device(system, AqualinkSensor, data={"name": "ph"}), - get_aqualink_device( - system, AqualinkThermostat, data={"name": "pool_set_point"} - ), + get_aqualink_device(system, name="aux_1", cls=IaquaAuxSwitch), + get_aqualink_device(system, name="freeze_protection", cls=IaquaBinarySensor), + get_aqualink_device(system, name="aux_2", cls=IaquaLightSwitch), + get_aqualink_device(system, name="ph", cls=IaquaSensor), + get_aqualink_device(system, name="pool_set_point", cls=IaquaThermostat), ] 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.""" config_entry.add_to_hass(hass) - system = get_aqualink_system(client) + system = get_aqualink_system(client, cls=IaquaSystem) systems = {system.serial: system} 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 await _ffwd_next_update_interval(hass) assert len(caplog.records) == 1 - assert "Reconnected" in caplog.text + assert "reconnected" in caplog.text # False -> None / ServiceException 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 await _ffwd_next_update_interval(hass) assert len(caplog.records) == 1 - assert "Reconnected" in caplog.text + assert "reconnected" in caplog.text # None -> False 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.""" config_entry.add_to_hass(hass) - system = get_aqualink_system(client) + system = get_aqualink_system(client, cls=IaquaSystem) systems = {system.serial: system} 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]} system.get_devices = AsyncMock(return_value=devices)