Co-authored-by: dev_agent <17+dev_agent@code.cubecraftcreations.com> Co-committed-by: dev_agent <17+dev_agent@code.cubecraftcreations.com>
215 lines
8.4 KiB
Python
215 lines
8.4 KiB
Python
"""Options flow for NestQuest post-setup settings.
|
|
|
|
Since the DB removal (Feature 18) the integration is a thin client of
|
|
the NestQuest API service: the settings that only served the local
|
|
database (the generation horizon, the day rollover, the notification
|
|
defaults, the admin allowlist) are gone — the API service owns those in
|
|
its own settings store, and the automations take their behaviour from
|
|
blueprint inputs. What remains is what the CLIENT itself consumes: the
|
|
panel-plane API connection (base URL + token), the shared coordinator's
|
|
refresh interval, the last-good staleness threshold, and the
|
|
panel-facing idle timeout.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
import voluptuous as vol
|
|
from homeassistant import config_entries
|
|
from homeassistant.data_entry_flow import FlowResult
|
|
|
|
from .const import (
|
|
CONF_API_BASE_URL,
|
|
CONF_PANEL_IDLE_TIMEOUT,
|
|
CONF_PANEL_TOKEN,
|
|
CONF_SNAPSHOT_STALENESS,
|
|
CONF_UPDATE_INTERVAL,
|
|
DEFAULT_API_BASE_URL,
|
|
DEFAULT_PANEL_IDLE_TIMEOUT,
|
|
DEFAULT_PANEL_TOKEN,
|
|
DEFAULT_SNAPSHOT_STALENESS,
|
|
DEFAULT_UPDATE_INTERVAL,
|
|
MIN_UPDATE_INTERVAL,
|
|
)
|
|
from .api_client import is_valid_base_url
|
|
|
|
MIN_PANEL_IDLE_TIMEOUT = 30
|
|
|
|
|
|
def _async_validate(
|
|
user_input: dict[str, Any],
|
|
current: dict[str, Any] | None = None,
|
|
) -> dict[str, str]:
|
|
"""Validate user input and return field-level errors instead of raising.
|
|
|
|
``current`` carries the stored values (entry options → entry data →
|
|
defaults) so a raw submit that sets one field but omits a sibling it
|
|
is validated against (the staleness threshold vs. the update
|
|
interval) can still check the effective pair; raw submits omitting
|
|
BOTH keys keep their stored values untouched.
|
|
"""
|
|
errors: dict[str, str] = {}
|
|
|
|
panel_idle_timeout = user_input.get(CONF_PANEL_IDLE_TIMEOUT)
|
|
if (
|
|
isinstance(panel_idle_timeout, bool)
|
|
or not isinstance(panel_idle_timeout, int)
|
|
or panel_idle_timeout < MIN_PANEL_IDLE_TIMEOUT
|
|
):
|
|
errors[CONF_PANEL_IDLE_TIMEOUT] = "invalid"
|
|
|
|
# The coordinator refresh interval (Feature 10) is OPTIONAL in raw
|
|
# submits: a missing key keeps the current value, so pre-Feature-10
|
|
# form payloads (and API callers) are unaffected. Schema-validated
|
|
# submits always carry it.
|
|
update_interval = user_input.get(CONF_UPDATE_INTERVAL)
|
|
if update_interval is not None and (
|
|
isinstance(update_interval, bool)
|
|
or not isinstance(update_interval, int)
|
|
or update_interval < MIN_UPDATE_INTERVAL
|
|
):
|
|
errors[CONF_UPDATE_INTERVAL] = "invalid"
|
|
|
|
# The last-good staleness threshold (Feature 18) is OPTIONAL in raw
|
|
# submits the same way: a missing key keeps the current value. It
|
|
# must be a positive int, and at least the EFFECTIVE update interval
|
|
# (the one this submit carries, else the stored or default one) — a
|
|
# threshold shorter than the poll cadence could never serve even one
|
|
# cached snapshot.
|
|
staleness = user_input.get(CONF_SNAPSHOT_STALENESS)
|
|
if staleness is not None and (
|
|
isinstance(staleness, bool)
|
|
or not isinstance(staleness, int)
|
|
or staleness < 1
|
|
):
|
|
errors[CONF_SNAPSHOT_STALENESS] = "invalid"
|
|
elif staleness is not None and not errors.get(CONF_UPDATE_INTERVAL):
|
|
stored_interval = (current or {}).get(CONF_UPDATE_INTERVAL)
|
|
effective_interval = update_interval
|
|
if effective_interval is None and (
|
|
isinstance(stored_interval, int)
|
|
and not isinstance(stored_interval, bool)
|
|
and stored_interval >= MIN_UPDATE_INTERVAL
|
|
):
|
|
effective_interval = stored_interval
|
|
if effective_interval is None:
|
|
effective_interval = DEFAULT_UPDATE_INTERVAL
|
|
if staleness < effective_interval:
|
|
errors[CONF_SNAPSHOT_STALENESS] = "invalid_staleness"
|
|
|
|
# The panel-plane API connection (Feature 18) is OPTIONAL in raw
|
|
# submits the same way: a missing key keeps the current value. The
|
|
# base URL must be a full http(s) URL with a host — the same check
|
|
# the client's constructor enforces, so a malformed URL fails at
|
|
# the form, not at the first request. The token may be EMPTY (the
|
|
# API connection is simply not configured yet) but a non-empty one
|
|
# must be pasted unpadded: a padded or whitespace-only value is a
|
|
# clipboard mistake that would fail the token check at request
|
|
# time.
|
|
api_base_url = user_input.get(CONF_API_BASE_URL)
|
|
if api_base_url is not None and not is_valid_base_url(api_base_url):
|
|
errors[CONF_API_BASE_URL] = "invalid_url"
|
|
panel_token = user_input.get(CONF_PANEL_TOKEN)
|
|
if panel_token is not None and (
|
|
not isinstance(panel_token, str)
|
|
or panel_token != panel_token.strip()
|
|
):
|
|
errors[CONF_PANEL_TOKEN] = "invalid"
|
|
|
|
return errors
|
|
|
|
|
|
def _build_schema(current: dict[str, Any]) -> vol.Schema:
|
|
"""Build the form schema, pre-filled with the current values.
|
|
|
|
The API connection (Feature 18) carries the base URL the API client
|
|
talks to and the panel service token it presents as the Bearer
|
|
credential. Schema-validated submits always carry every field (the
|
|
defaults pre-fill from the stored values); raw callers omitting one
|
|
keep its stored value (see _async_validate).
|
|
"""
|
|
return vol.Schema(
|
|
{
|
|
vol.Required(
|
|
CONF_PANEL_IDLE_TIMEOUT,
|
|
default=current[CONF_PANEL_IDLE_TIMEOUT],
|
|
): int,
|
|
vol.Required(
|
|
CONF_UPDATE_INTERVAL, default=current[CONF_UPDATE_INTERVAL]
|
|
): int,
|
|
# The last-good staleness threshold (Feature 18): how long
|
|
# the coordinator keeps serving the cached snapshot after
|
|
# the API stops answering.
|
|
vol.Required(
|
|
CONF_SNAPSHOT_STALENESS,
|
|
default=current[CONF_SNAPSHOT_STALENESS],
|
|
): int,
|
|
# Panel-plane API connection (Feature 18): the base URL the
|
|
# API client talks to and the panel service token it
|
|
# presents as the Bearer credential.
|
|
vol.Required(
|
|
CONF_API_BASE_URL, default=current[CONF_API_BASE_URL]
|
|
): str,
|
|
vol.Required(
|
|
CONF_PANEL_TOKEN, default=current[CONF_PANEL_TOKEN]
|
|
): str,
|
|
}
|
|
)
|
|
|
|
|
|
class NestQuestOptionsFlow(config_entries.OptionsFlowWithConfigEntry):
|
|
"""Handle the options flow for NestQuest."""
|
|
|
|
async def async_step_init(
|
|
self, user_input: dict[str, Any] | None = None
|
|
) -> FlowResult:
|
|
"""Manage the NestQuest options."""
|
|
if user_input is None:
|
|
return self.async_show_form(
|
|
step_id="init",
|
|
data_schema=_build_schema(self._current_values()),
|
|
)
|
|
|
|
errors = _async_validate(user_input, self._current_values())
|
|
if errors:
|
|
return self.async_show_form(
|
|
step_id="init",
|
|
data_schema=_build_schema(self._current_values()),
|
|
errors=errors,
|
|
)
|
|
|
|
# Every remaining key is stored the fall-back-to-current way, so
|
|
# raw submits that predate a section keep that section's stored
|
|
# values.
|
|
current = self._current_values()
|
|
data: dict[str, Any] = {}
|
|
for key in (
|
|
CONF_PANEL_IDLE_TIMEOUT,
|
|
CONF_UPDATE_INTERVAL,
|
|
CONF_SNAPSHOT_STALENESS,
|
|
CONF_API_BASE_URL,
|
|
CONF_PANEL_TOKEN,
|
|
):
|
|
data[key] = user_input.get(key, current[key])
|
|
return self.async_create_entry(title="NestQuest", data=data)
|
|
|
|
def _current_values(self) -> dict[str, Any]:
|
|
"""Resolve current values from entry options, then entry data, then defaults."""
|
|
options = self.options
|
|
entry_data = getattr(self.config_entry, "data", None) or {}
|
|
values: dict[str, Any] = {}
|
|
for key, default in (
|
|
(CONF_PANEL_IDLE_TIMEOUT, DEFAULT_PANEL_IDLE_TIMEOUT),
|
|
(CONF_UPDATE_INTERVAL, DEFAULT_UPDATE_INTERVAL),
|
|
(CONF_SNAPSHOT_STALENESS, DEFAULT_SNAPSHOT_STALENESS),
|
|
(CONF_API_BASE_URL, DEFAULT_API_BASE_URL),
|
|
(CONF_PANEL_TOKEN, DEFAULT_PANEL_TOKEN),
|
|
):
|
|
if key in options:
|
|
values[key] = options[key]
|
|
elif key in entry_data:
|
|
values[key] = entry_data[key]
|
|
else:
|
|
values[key] = default
|
|
return values
|