From 0beaa71fdc61c4158259f704a9bfd17b9020275b Mon Sep 17 00:00:00 2001 From: Sebastien Lavoie Date: Sun, 29 Mar 2026 08:44:43 -0400 Subject: [PATCH] feat: add DknCloudNaClient stub --- custom_components/dkncloudna/api.py | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 custom_components/dkncloudna/api.py diff --git a/custom_components/dkncloudna/api.py b/custom_components/dkncloudna/api.py new file mode 100644 index 0000000..773defd --- /dev/null +++ b/custom_components/dkncloudna/api.py @@ -0,0 +1,125 @@ +"""DKN Cloud NA API client stub. + +Real HTTP calls are implemented in a future phase. This stub defines the +interface and returns hardcoded data so the rest of the integration can be +developed and tested without live credentials. +""" + +from __future__ import annotations + +from typing import Any + +from aiohttp import ClientSession + +from .const import LOGGER + + +class DknAuthError(Exception): + """Raised when authentication fails (401).""" + + +class DknConnectionError(Exception): + """Raised when the API cannot be reached.""" + + +class DknCloudNaClient: + """Async client for the DKN Cloud NA REST API. + + Instantiated once per config entry. Password is cleared after login and + never stored beyond the initial token exchange. + """ + + def __init__( + self, + username: str, + session: ClientSession, + *, + password: str | None = None, + token: str | None = None, + refresh_token: str | None = None, + ) -> None: + self._username = username + self._session = session + self._password = password + self.token = token + self.refresh_token = refresh_token + + def clear_password(self) -> None: + """Discard password from memory after token exchange.""" + self._password = None + + async def login(self) -> None: + """Exchange email+password for access and refresh tokens. + + Sets self.token and self.refresh_token on success. + Raises DknAuthError on bad credentials, DknConnectionError on network failure. + """ + # TODO: implement real POST to API_LOGIN + LOGGER.debug("DknCloudNaClient.login() stub called for %s", self._username) + raise NotImplementedError("login() not yet implemented") + + async def is_logged_in(self) -> bool: + """Return True if the current token is still valid.""" + # TODO: implement real GET to API_IS_LOGGED_IN + raise NotImplementedError("is_logged_in() not yet implemented") + + async def refresh_access_token(self) -> None: + """Use the refresh token to obtain a new access token. + + Updates self.token on success. + Raises DknAuthError if the refresh token is also expired. + """ + # TODO: implement real GET to API_REFRESH_TOKEN + raise NotImplementedError("refresh_access_token() not yet implemented") + + async def fetch_installations(self) -> list[dict[str, Any]]: + """Return all installations and their devices. + + Each installation contains a list of DeviceInfo dicts. + Returns stub data for scaffolding. + """ + LOGGER.debug("DknCloudNaClient.fetch_installations() stub — returning fake data") + return [ + { + "_id": "stub-installation-1", + "name": "My Home", + "devices": [ + { + "mac": "aa:bb:cc:dd:ee:ff", + "name": "Living Room AC", + "power": False, + "mode": 1, + "real_mode": 1, + "work_temp": 22.0, + "ext_temp": 18.0, + "units": 0, + "setpoint_air_auto": 22.0, + "setpoint_air_cool": 24.0, + "setpoint_air_heat": 20.0, + "range_sp_auto_air_min": 16, + "range_sp_auto_air_max": 32, + "range_sp_cool_air_min": 16, + "range_sp_cool_air_max": 32, + "range_sp_hot_air_min": 16, + "range_sp_hot_air_max": 32, + "speed_state": 0, + "speed_available": [0, 2, 3, 4, 5, 6], + "slats_vertical_1": 0, + "machineready": True, + "isConnected": True, + "tsensor_error": False, + "stat_rssi": -65, + "stat_ssid": "MyWiFi", + "version": "1.0.0", + "error_value": 0, + "error_ascii1": "", + "error_ascii2": "", + } + ], + } + ] + + def __repr__(self) -> str: + first = self._username[0] if self._username else "?" + token_state = "set" if self.token else "none" + return f"DknCloudNaClient(u={first}***, token={token_state})"