Skip to content

Commit

Permalink
Update Aseko to support new API (#126133)
Browse files Browse the repository at this point in the history
* Update Aseko to support new API

* Apply suggestions from code review

Co-authored-by: Joost Lekkerkerker <[email protected]>

* Use self.unit instead of self._unit

* Refactor sensor setup entry

* Keep same unique id and identifier

* Revert rename free_chlorine translation key

* Remove new heating entity to keep PR small

* Fix keep same unique id

---------

Co-authored-by: Joost Lekkerkerker <[email protected]>
  • Loading branch information
milanmeu and joostlek authored Sep 18, 2024
1 parent e2f1c60 commit 12dbabb
Show file tree
Hide file tree
Showing 13 changed files with 222 additions and 176 deletions.
29 changes: 8 additions & 21 deletions homeassistant/components/aseko_pool_live/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@

import logging

from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount
from aioaseko import Aseko, AsekoNotLoggedIn

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.exceptions import ConfigEntryAuthFailed

from .const import DOMAIN
from .coordinator import AsekoDataUpdateCoordinator
Expand All @@ -22,36 +21,24 @@

async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Aseko Pool Live from a config entry."""
account = MobileAccount(
async_get_clientsession(hass),
username=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD],
)
aseko = Aseko(entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])

try:
units = await account.get_units()
except InvalidAuthCredentials as err:
await aseko.login()
except AsekoNotLoggedIn as err:
raise ConfigEntryAuthFailed from err
except APIUnavailable as err:
raise ConfigEntryNotReady from err

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = []

for unit in units:
coordinator = AsekoDataUpdateCoordinator(hass, unit)
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id].append((unit, coordinator))

coordinator = AsekoDataUpdateCoordinator(hass, aseko)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


Expand Down
52 changes: 14 additions & 38 deletions homeassistant/components/aseko_pool_live/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from aioaseko import Unit

from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
Expand All @@ -25,26 +24,14 @@
class AsekoBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes an Aseko binary sensor entity."""

value_fn: Callable[[Unit], bool]
value_fn: Callable[[Unit], bool | None]


UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
AsekoBinarySensorEntityDescription(
key="water_flow",
translation_key="water_flow",
value_fn=lambda unit: unit.water_flow,
),
AsekoBinarySensorEntityDescription(
key="has_alarm",
translation_key="alarm",
value_fn=lambda unit: unit.has_alarm,
device_class=BinarySensorDeviceClass.SAFETY,
),
AsekoBinarySensorEntityDescription(
key="has_error",
translation_key="error",
value_fn=lambda unit: unit.has_error,
device_class=BinarySensorDeviceClass.PROBLEM,
translation_key="water_flow_to_probes",
value_fn=lambda unit: unit.water_flow_to_probes,
),
)

Expand All @@ -55,33 +42,22 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Aseko Pool Live binary sensors."""
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
config_entry.entry_id
]
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
units = coordinator.data.values()
async_add_entities(
AsekoUnitBinarySensorEntity(unit, coordinator, description)
for unit, coordinator in data
for description in UNIT_BINARY_SENSORS
AsekoBinarySensorEntity(unit, coordinator, description)
for description in BINARY_SENSORS
for unit in units
if description.value_fn(unit) is not None
)


class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity):
"""Representation of a unit water flow binary sensor entity."""
class AsekoBinarySensorEntity(AsekoEntity, BinarySensorEntity):
"""Representation of an Aseko binary sensor entity."""

entity_description: AsekoBinarySensorEntityDescription

def __init__(
self,
unit: Unit,
coordinator: AsekoDataUpdateCoordinator,
entity_description: AsekoBinarySensorEntityDescription,
) -> None:
"""Initialize the unit binary sensor."""
super().__init__(unit, coordinator)
self.entity_description = entity_description
self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}"

@property
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._unit)
return self.entity_description.value_fn(self.unit)
20 changes: 8 additions & 12 deletions homeassistant/components/aseko_pool_live/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@
import logging
from typing import Any

from aioaseko import APIUnavailable, InvalidAuthCredentials, WebAccount
from aioaseko import Aseko, AsekoAPIError, AsekoInvalidCredentials
import voluptuous as vol

from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from .const import DOMAIN

Expand All @@ -34,15 +33,12 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):

async def get_account_info(self, email: str, password: str) -> dict:
"""Get account info from the mobile API and the web API."""
session = async_get_clientsession(self.hass)

web_account = WebAccount(session, email, password)
web_account_info = await web_account.login()

aseko = Aseko(email, password)
user = await aseko.login()
return {
CONF_EMAIL: email,
CONF_PASSWORD: password,
CONF_UNIQUE_ID: web_account_info.user_id,
CONF_UNIQUE_ID: user.user_id,
}

async def async_step_user(
Expand All @@ -58,9 +54,9 @@ async def async_step_user(
info = await self.get_account_info(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except APIUnavailable:
except AsekoAPIError:
errors["base"] = "cannot_connect"
except InvalidAuthCredentials:
except AsekoInvalidCredentials:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
Expand Down Expand Up @@ -122,9 +118,9 @@ async def async_step_reauth_confirm(
info = await self.get_account_info(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except APIUnavailable:
except AsekoAPIError:
errors["base"] = "cannot_connect"
except InvalidAuthCredentials:
except AsekoInvalidCredentials:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
Expand Down
23 changes: 10 additions & 13 deletions homeassistant/components/aseko_pool_live/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,31 @@
from datetime import timedelta
import logging

from aioaseko import Unit, Variable
from aioaseko import Aseko, Unit

from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator

from .const import DOMAIN

_LOGGER = logging.getLogger(__name__)


class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]):
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Unit]]):
"""Class to manage fetching Aseko unit data from single endpoint."""

def __init__(self, hass: HomeAssistant, unit: Unit) -> None:
def __init__(self, hass: HomeAssistant, aseko: Aseko) -> None:
"""Initialize global Aseko unit data updater."""
self._unit = unit

if self._unit.name:
name = self._unit.name
else:
name = f"{self._unit.type}-{self._unit.serial_number}"
self._aseko = aseko

super().__init__(
hass,
_LOGGER,
name=name,
name=DOMAIN,
update_interval=timedelta(minutes=2),
)

async def _async_update_data(self) -> dict[str, Variable]:
async def _async_update_data(self) -> dict[str, Unit]:
"""Fetch unit data."""
await self._unit.get_state()
return {variable.type: variable for variable in self._unit.variables}
units = await self._aseko.get_units()
return {unit.serial_number: unit for unit in units}
49 changes: 37 additions & 12 deletions homeassistant/components/aseko_pool_live/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from aioaseko import Unit

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
Expand All @@ -14,20 +15,44 @@ class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):

_attr_has_entity_name = True

def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None:
def __init__(
self,
unit: Unit,
coordinator: AsekoDataUpdateCoordinator,
description: EntityDescription,
) -> None:
"""Initialize the aseko entity."""
super().__init__(coordinator)
self.entity_description = description
self._unit = unit

if self._unit.type == "Remote":
self._device_model = "ASIN Pool"
else:
self._device_model = f"ASIN AQUA {self._unit.type}"
self._device_name = self._unit.name if self._unit.name else self._device_model

self._attr_unique_id = f"{self.unit.serial_number}{self.entity_description.key}"
self._attr_device_info = DeviceInfo(
name=self._device_name,
identifiers={(DOMAIN, str(self._unit.serial_number))},
manufacturer="Aseko",
model=self._device_model,
identifiers={(DOMAIN, self.unit.serial_number)},
serial_number=self.unit.serial_number,
name=unit.name or unit.serial_number,
manufacturer=(
self.unit.brand_name.primary
if self.unit.brand_name is not None
else None
),
model=(
self.unit.brand_name.secondary
if self.unit.brand_name is not None
else None
),
configuration_url=f"https://aseko.cloud/unit/{self.unit.serial_number}",
)

@property
def unit(self) -> Unit:
"""Return the aseko unit."""
return self.coordinator.data[self._unit.serial_number]

@property
def available(self) -> bool:
"""Return True if entity is available."""
return (
super().available
and self.unit.serial_number in self.coordinator.data
and self.unit.online
)
15 changes: 12 additions & 3 deletions homeassistant/components/aseko_pool_live/icons.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
{
"entity": {
"binary_sensor": {
"water_flow": {
"water_flow_to_probes": {
"default": "mdi:waves-arrow-right"
}
},
"sensor": {
"air_temperature": {
"default": "mdi:thermometer-lines"
},
"free_chlorine": {
"default": "mdi:flask"
"default": "mdi:pool"
},
"redox": {
"default": "mdi:pool"
},
"salinity": {
"default": "mdi:pool"
},
"water_temperature": {
"default": "mdi:coolant-temperature"
"default": "mdi:pool-thermometer"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/aseko_pool_live/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
"iot_class": "cloud_polling",
"loggers": ["aioaseko"],
"requirements": ["aioaseko==0.2.0"]
"requirements": ["aioaseko==1.0.0"]
}
Loading

0 comments on commit 12dbabb

Please sign in to comment.