Merge pull request 'Treat 404 as auth error and persist refreshed tokens' (#1) from fix/token-refresh-404 into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user