diff --git a/custom_components/dkncloudna/__init__.py b/custom_components/dkncloudna/__init__.py index 3db5fe9..db028d2 100644 --- a/custom_components/dkncloudna/__init__.py +++ b/custom_components/dkncloudna/__init__.py @@ -9,9 +9,18 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from .api import DknCloudNaClient -from .const import CONF_REFRESH_TOKEN, CONF_USER_TOKEN, DOMAIN, LOGGER +from .const import ( + CONF_EXPOSE_PII, + CONF_REFRESH_TOKEN, + CONF_SCAN_INTERVAL, + CONF_USER_TOKEN, + DOMAIN, + LOGGER, +) from .coordinator import DknCoordinator +_RELOAD_KEYS = {CONF_SCAN_INTERVAL, CONF_EXPOSE_PII} + PLATFORMS: list[Platform] = [ Platform.CLIMATE, Platform.SENSOR, @@ -44,6 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN][entry.entry_id] = { "coordinator": coordinator, "client": client, + "_prev_options": dict(entry.options), } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) @@ -74,5 +84,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: - """Reload entry when options change.""" - await hass.config_entries.async_reload(entry.entry_id) + """Reload entry when user-facing options change (not token refreshes).""" + domain_data = hass.data.get(DOMAIN, {}).get(entry.entry_id, {}) + prev_opts = domain_data.get("_prev_options", {}) + if any(entry.options.get(k) != prev_opts.get(k) for k in _RELOAD_KEYS): + await hass.config_entries.async_reload(entry.entry_id) + domain_data["_prev_options"] = dict(entry.options) diff --git a/custom_components/dkncloudna/api.py b/custom_components/dkncloudna/api.py index 10d60b9..a3b9b52 100644 --- a/custom_components/dkncloudna/api.py +++ b/custom_components/dkncloudna/api.py @@ -90,7 +90,7 @@ class DknCloudNaClient: "GET", API_IS_LOGGED_IN, require_auth=True, - auth_error_statuses={401, 403}, + auth_error_statuses={401, 403, 404}, ) except DknAuthError: return False @@ -106,7 +106,7 @@ class DknCloudNaClient: "GET", API_REFRESH_TOKEN.format(refresh_token=self.refresh_token), require_auth=bool(self.token), - auth_error_statuses={400, 401, 403}, + auth_error_statuses={400, 401, 403, 404}, ) self._store_tokens(data) @@ -117,7 +117,7 @@ class DknCloudNaClient: API_INSTALLATIONS, require_auth=True, retry_on_auth=True, - auth_error_statuses={401, 403}, + auth_error_statuses={401, 403, 404}, ) if not isinstance(data, list): raise DknConnectionError("Unexpected installations response") @@ -397,10 +397,20 @@ class DknCloudNaClient: except ClientError as err: raise DknConnectionError(str(err) or type(err).__name__) from err - LOGGER.debug("DKN response %s %s status=%s", method, url, response.status) + if response.status >= 400: + LOGGER.warning( + "DKN API error %s %s status=%s body=%s", + method, + url, + response.status, + data if not isinstance(data, str) or len(data) < 200 else data[:200], + ) + else: + LOGGER.debug("DKN response %s %s status=%s", method, url, response.status) if response.status in (auth_error_statuses or set()): if retry_on_auth and self.refresh_token: + LOGGER.info("DKN token expired, attempting refresh") await self.refresh_access_token() return await self._request( method, diff --git a/custom_components/dkncloudna/coordinator.py b/custom_components/dkncloudna/coordinator.py index ef8a0db..f1afa6c 100644 --- a/custom_components/dkncloudna/coordinator.py +++ b/custom_components/dkncloudna/coordinator.py @@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .api import DknAuthError, DknCloudNaClient, DknConnectionError -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER +from .const import CONF_REFRESH_TOKEN, CONF_USER_TOKEN, DEFAULT_SCAN_INTERVAL, DOMAIN, LOGGER class DknCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]): @@ -64,6 +64,8 @@ class DknCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]): except Exception as err: # noqa: BLE001 raise UpdateFailed(f"Unexpected error: {type(err).__name__}") from err + self._persist_tokens_if_changed() + devices: dict[str, dict[str, Any]] = {} existing = self.data or {} for installation in installations or []: @@ -80,6 +82,25 @@ class DknCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]): return devices + def _persist_tokens_if_changed(self) -> None: + """Save refreshed tokens back to the config entry so they survive restarts.""" + opts = self._entry.options + stored_token = opts.get(CONF_USER_TOKEN) + stored_refresh = opts.get(CONF_REFRESH_TOKEN) + if ( + self.client.token + and self.client.refresh_token + and ( + self.client.token != stored_token + or self.client.refresh_token != stored_refresh + ) + ): + new_opts = dict(opts) + new_opts[CONF_USER_TOKEN] = self.client.token + new_opts[CONF_REFRESH_TOKEN] = self.client.refresh_token + self.hass.config_entries.async_update_entry(self._entry, options=new_opts) + LOGGER.debug("DKN tokens persisted after refresh") + async def async_handle_socket_device_data( self, mac: str, data: dict[str, Any] ) -> None: