from __future__ import annotations import logging import time from typing import TYPE_CHECKING, Any, ClassVar from iaqualink.exception import ( AqualinkServiceException, AqualinkServiceThrottledException, AqualinkSystemOfflineException, ) from iaqualink.system import AqualinkSystem from iaqualink.systems.exo.device import ExoDevice if TYPE_CHECKING: import httpx from iaqualink.client import AqualinkClient from iaqualink.typing import Payload EXO_DEVICES_URL = "https://prod.zodiac-io.com/devices/v1" LOGGER = logging.getLogger("iaqualink") class ExoSystem(AqualinkSystem): NAME = "exo" # Empiric value; eXO systems returned HTTP 429 when polled every 15s. MIN_SECS_TO_REFRESH: ClassVar[int] = 50 def __init__(self, aqualink: AqualinkClient, data: Payload): super().__init__(aqualink, data) # This lives in the parent class but mypy complains. self.last_refresh: int = 0 self.temp_unit = "C" # TODO: check if unit can be changed on panel? def __repr__(self) -> str: attrs = ["name", "serial", "data"] attrs = [f"{i}={getattr(self, i)!r}" for i in attrs] return f"{self.__class__.__name__}({' '.join(attrs)})" async def send_devices_request(self, **kwargs: Any) -> httpx.Response: async def do_request() -> httpx.Response: url = f"{EXO_DEVICES_URL}/{self.serial}/shadow" headers = {"Authorization": self.aqualink.id_token} return await self.aqualink.send_request( url, headers=headers, **kwargs, ) return await self._send_with_reauth_retry(do_request) async def send_reported_state_request(self) -> httpx.Response: return await self.send_devices_request() async def send_desired_state_request( self, state: dict[str, Any] ) -> httpx.Response: return await self.send_devices_request( method="post", json={"state": {"desired": state}} ) async def update(self) -> None: # Be nice to Aqualink servers since we rely on polling. now = int(time.time()) delta = now - self.last_refresh if delta < self.MIN_SECS_TO_REFRESH: LOGGER.debug(f"Only {delta}s since last refresh.") return try: r = await self.send_reported_state_request() except AqualinkServiceThrottledException: # Re-raise without setting online=None; rate-limiting does # not indicate the system is offline. raise except AqualinkServiceException: self.online = None raise try: self._parse_shadow_response(r) except AqualinkSystemOfflineException: self.online = False raise self.online = True self.last_refresh = int(time.time()) def _parse_shadow_response(self, response: httpx.Response) -> None: data = response.json() LOGGER.debug(f"Shadow response: {data}") devices = {} # Process the chlorinator attributes[equipmen] # Make the data a bit flatter. root = data["state"]["reported"]["equipment"]["swc_0"] for name, state in root.items(): attrs = {"name": name} if isinstance(state, dict): attrs.update(state) else: attrs.update({"state": state}) devices.update({name: attrs}) # Remove those values, they're not handled properly. devices.pop("boost_time", None) devices.pop("vsp_speed", None) devices.pop("sn", None) devices.pop("vr", None) devices.pop("version", None) # Process the heating control attributes if "heating" in data["state"]["reported"]: name = "heating" attrs = {"name": name} attrs.update(data["state"]["reported"]["heating"]) devices.update({name: attrs}) # extract heater state into seperate device to maintain homeassistant API name = "heater" attrs = {"name": name} attrs.update( {"state": data["state"]["reported"]["heating"]["state"]} ) devices.update({name: attrs}) LOGGER.debug(f"devices: {devices}") for k, v in devices.items(): if k in self.devices: for dk, dv in v.items(): self.devices[k].data[dk] = dv else: self.devices[k] = ExoDevice.from_data(self, v) async def set_heating(self, name: str, state: int) -> None: r = await self.send_desired_state_request({"heating": {name: state}}) r.raise_for_status() async def set_aux(self, aux: str, state: int) -> None: r = await self.send_desired_state_request( {"equipment": {"swc_0": {aux: {"state": state}}}} ) r.raise_for_status() async def set_toggle(self, name: str, state: int) -> None: r = await self.send_desired_state_request( {"equipment": {"swc_0": {name: state}}} ) r.raise_for_status() async def set_filter_pump(self, name: str, state: int) -> None: r = await self.send_desired_state_request( {"equipment": {"swc_0": {name: {"state": state}}}} ) r.raise_for_status()