Files
homeassistant-dkncloudna/custom_components/dkncloudna/__init__.py
T
thatguygriff dd1cc76580
Validate / Hassfest validation (pull_request) Failing after 20s
Validate / HACS validation (pull_request) Failing after 0s
Treat 404 as auth error and persist refreshed tokens
The DKN Cloud NA API returns 404 (not 401) when tokens are expired,
which caused the integration to silently fail with UpdateFailed
indefinitely instead of attempting a token refresh or triggering
reauth. Refreshed tokens were also only held in memory, so they
were lost on Home Assistant restart.

- Add 404 to auth_error_statuses on is_logged_in, refresh_access_token,
  and fetch_installations.
- Persist refreshed access + refresh tokens back to the config entry
  after each successful coordinator update.
- Skip the entry reload listener for token-only option updates to
  avoid a reload loop on every refresh.
- Log API 4xx responses at WARNING with the body so failures are
  visible in HA logs without enabling debug logging.
2026-05-26 09:58:57 -03:00

93 lines
2.9 KiB
Python

"""DKN Cloud NA integration for Home Assistant."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .api import DknCloudNaClient
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,
Platform.BINARY_SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up DKN Cloud NA from a config entry."""
hass.data.setdefault(DOMAIN, {})
token = entry.options.get(CONF_USER_TOKEN)
if not token:
raise ConfigEntryAuthFailed("No token in options; reauthentication required")
username = entry.data.get("username", "")
refresh_token = entry.options.get(CONF_REFRESH_TOKEN)
session = async_get_clientsession(hass)
client = DknCloudNaClient(
username,
session,
token=token,
refresh_token=refresh_token,
)
coordinator = DknCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh()
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)
entry.async_on_unload(entry.add_update_listener(_async_reload_entry))
LOGGER.info(
"DKN Cloud NA set up (entry=%s, scan_interval=%ss)",
entry.entry_id,
coordinator.update_interval.total_seconds()
if coordinator.update_interval
else "?",
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
client: DknCloudNaClient | None = (
hass.data.get(DOMAIN, {}).get(entry.entry_id, {}).get("client")
)
if client is not None:
await client.disconnect_socket()
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id, None)
return unload_ok
async def _async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""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)