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 homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
from .api import DknCloudNaClient
|
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
|
from .coordinator import DknCoordinator
|
||||||
|
|
||||||
|
_RELOAD_KEYS = {CONF_SCAN_INTERVAL, CONF_EXPOSE_PII}
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [
|
PLATFORMS: list[Platform] = [
|
||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
@@ -44,6 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass.data[DOMAIN][entry.entry_id] = {
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
"coordinator": coordinator,
|
"coordinator": coordinator,
|
||||||
"client": client,
|
"client": client,
|
||||||
|
"_prev_options": dict(entry.options),
|
||||||
}
|
}
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
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:
|
async def _async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
"""Reload entry when options change."""
|
"""Reload entry when user-facing options change (not token refreshes)."""
|
||||||
await hass.config_entries.async_reload(entry.entry_id)
|
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",
|
"GET",
|
||||||
API_IS_LOGGED_IN,
|
API_IS_LOGGED_IN,
|
||||||
require_auth=True,
|
require_auth=True,
|
||||||
auth_error_statuses={401, 403},
|
auth_error_statuses={401, 403, 404},
|
||||||
)
|
)
|
||||||
except DknAuthError:
|
except DknAuthError:
|
||||||
return False
|
return False
|
||||||
@@ -106,7 +106,7 @@ class DknCloudNaClient:
|
|||||||
"GET",
|
"GET",
|
||||||
API_REFRESH_TOKEN.format(refresh_token=self.refresh_token),
|
API_REFRESH_TOKEN.format(refresh_token=self.refresh_token),
|
||||||
require_auth=bool(self.token),
|
require_auth=bool(self.token),
|
||||||
auth_error_statuses={400, 401, 403},
|
auth_error_statuses={400, 401, 403, 404},
|
||||||
)
|
)
|
||||||
self._store_tokens(data)
|
self._store_tokens(data)
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ class DknCloudNaClient:
|
|||||||
API_INSTALLATIONS,
|
API_INSTALLATIONS,
|
||||||
require_auth=True,
|
require_auth=True,
|
||||||
retry_on_auth=True,
|
retry_on_auth=True,
|
||||||
auth_error_statuses={401, 403},
|
auth_error_statuses={401, 403, 404},
|
||||||
)
|
)
|
||||||
if not isinstance(data, list):
|
if not isinstance(data, list):
|
||||||
raise DknConnectionError("Unexpected installations response")
|
raise DknConnectionError("Unexpected installations response")
|
||||||
@@ -397,10 +397,20 @@ class DknCloudNaClient:
|
|||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
raise DknConnectionError(str(err) or type(err).__name__) from 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 response.status in (auth_error_statuses or set()):
|
||||||
if retry_on_auth and self.refresh_token:
|
if retry_on_auth and self.refresh_token:
|
||||||
|
LOGGER.info("DKN token expired, attempting refresh")
|
||||||
await self.refresh_access_token()
|
await self.refresh_access_token()
|
||||||
return await self._request(
|
return await self._request(
|
||||||
method,
|
method,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
|
|||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .api import DknAuthError, DknCloudNaClient, DknConnectionError
|
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]]]):
|
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
|
except Exception as err: # noqa: BLE001
|
||||||
raise UpdateFailed(f"Unexpected error: {type(err).__name__}") from err
|
raise UpdateFailed(f"Unexpected error: {type(err).__name__}") from err
|
||||||
|
|
||||||
|
self._persist_tokens_if_changed()
|
||||||
|
|
||||||
devices: dict[str, dict[str, Any]] = {}
|
devices: dict[str, dict[str, Any]] = {}
|
||||||
existing = self.data or {}
|
existing = self.data or {}
|
||||||
for installation in installations or []:
|
for installation in installations or []:
|
||||||
@@ -80,6 +82,25 @@ class DknCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
|
|||||||
|
|
||||||
return devices
|
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(
|
async def async_handle_socket_device_data(
|
||||||
self, mac: str, data: dict[str, Any]
|
self, mac: str, data: dict[str, Any]
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user