Treat 404 as auth error and persist refreshed tokens #1

Merged
thatguygriff merged 1 commits from fix/token-refresh-404 into main 2026-05-26 13:08:19 +00:00
3 changed files with 53 additions and 8 deletions
+17 -3
View File
@@ -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)
+14 -4
View File
@@ -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,
+22 -1
View File
@@ -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: