Auto-register the zero-config panel dashboard on setup

A fresh HA install with children configured should show the panel
with no YAML and no manual dashboard creation: setup now registers
ONE storage-mode Lovelace dashboard whose config is the bundle's
custom:nestquest-party strategy (frontend/src/strategy.ts), under the
stable url_path 'nestquest' with a title, icon and sidebar visibility.
Registration is idempotent — the url_path is the idempotency key, so
a reload or a second entry never duplicates the dashboard, and a
dashboard already occupying the url_path (ours or the user's) is left
completely alone (never overwritten, never deleted).  An unavailable
Lovelace dashboards collection (YAML mode, lovelace not loaded) or a
refused create logs a clear warning and setup continues — never a
crash.  Tests cover create-on-setup, no-duplicate-on-reload, the
untouched user dashboard, the missing collection and a refusing one.
This commit is contained in:
2026-09-23 10:17:20 -04:00
parent 6d570e1c0e
commit f668cf95b0
4 changed files with 245 additions and 1 deletions
+10 -1
View File
@@ -5,7 +5,8 @@ SQLite file, the migrations, and every business rule. Setup builds the
entry's ONE shared API client, the coordinator that polls the panel
snapshot through it, and the SSE subscription that re-fires the API's
transition frames on the HA bus, registers the single
``nestquest.complete_quest`` proxy service, and forwards the sensor and
``nestquest.complete_quest`` proxy service, registers the zero-config
panel dashboard (Feature 20), and forwards the sensor and
binary_sensor platforms.
"""
from __future__ import annotations
@@ -29,6 +30,7 @@ from .const import (
PLATFORMS,
)
from .coordinator import NestQuestCoordinator, coordinator_client_from_entry
from .dashboard import async_register_dashboard
from .frontend import async_register_frontend
from .services import async_deregister_services, async_register_services
from .sse import NestQuestEventStream
@@ -87,6 +89,13 @@ async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up NestQuest from a config entry."""
await async_register_frontend(hass)
# The zero-config panel dashboard (Feature 20): register the
# storage-mode strategy dashboard if no dashboard under the stable
# url_path exists yet. Unavailability of the Lovelace dashboards
# collection (YAML mode, lovelace not loaded) logs a warning and
# NEVER fails setup; an existing dashboard — ours or the user's —
# is left untouched, so a reload cannot duplicate it.
await async_register_dashboard(hass)
hass.data.setdefault(DOMAIN, {})
async def _async_update_listener(
+12
View File
@@ -43,6 +43,18 @@ DOMAIN_SERVICES: tuple[str, ...] = (
SQLITE_DB_FILENAME = "nestquest.db"
# Zero-config panel dashboard (Feature 20): ONE storage-mode Lovelace
# dashboard registered on setup whose config carries the bundle's
# ``custom:nestquest-party`` strategy (frontend/src/strategy.ts) — the
# views are generated at render time from the household roster. The
# url_path IS the domain: the stable key idempotency is keyed on (a
# reload or a second entry never duplicates it, and a dashboard already
# registered under it — ours or the user's — is left untouched).
DASHBOARD_URL_PATH = DOMAIN
DASHBOARD_TITLE = "NestQuest"
DASHBOARD_ICON = "mdi:party-popper"
DASHBOARD_STRATEGY_TYPE = "custom:nestquest-party"
# Configuration keys and default values
CONF_HORIZON_DAYS = "horizon_days"
DEFAULT_HORIZON_DAYS = 14
+113
View File
@@ -0,0 +1,113 @@
"""Zero-config panel dashboard registration for NestQuest (Feature 20).
On setup the integration registers ONE storage-mode Lovelace dashboard
whose config uses the bundle's ``custom:nestquest-party`` strategy (see
``frontend/src/strategy.ts``): the views themselves are GENERATED at
render time from the household roster, so the stored dashboard carries
nothing but the strategy reference. Registration is IDEMPOTENT — a
dashboard with the stable ``url_path`` (ours or a user's) is never
touched, and the Lovelace dashboards collection being unavailable
(YAML mode, lovelace not loaded) is a logged warning, never a setup
failure.
"""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from .const import (
DASHBOARD_ICON,
DASHBOARD_STRATEGY_TYPE,
DASHBOARD_TITLE,
DASHBOARD_URL_PATH,
LOGGER,
)
_LOGGER = LOGGER
def _dashboards_collection(hass: HomeAssistant):
"""Return the Lovelace dashboards collection, or None when absent.
The lovelace integration stashes its collections on
``hass.data["lovelace"]``; a missing key (lovelace not loaded) and a
collection-less LovelaceData both mean "cannot create storage
dashboards here" — callers log and continue. The presence of the
surface is verified by capability (``async_create_item`` callable),
not by type, so no lovelace import is needed.
"""
lovelace_data = hass.data.get("lovelace")
collection = getattr(lovelace_data, "dashboards", None)
if collection is None or not callable(
getattr(collection, "async_create_item", None)
):
return None
return collection
def _item_url_path(item) -> str | None:
"""Read a dashboard item's ``url_path`` through either access shape.
The collection hands back storage items as dicts while builtin
dashboards come back as config objects; the url_path is reachable
through ``item["url_path"]`` or ``item.url_path`` respectively.
"""
url_path = getattr(item, "url_path", None)
if isinstance(url_path, str):
return url_path
get_url_path = getattr(item, "get", None)
if callable(get_url_path):
value = get_url_path("url_path")
if isinstance(value, str):
return value
return None
def _existing_dashboard(items) -> bool:
"""Whether a dashboard under the stable url_path already exists."""
return any(_item_url_path(item) == DASHBOARD_URL_PATH for item in items)
async def async_register_dashboard(hass: HomeAssistant) -> bool:
"""Register the NestQuest panel dashboard exactly once.
Returns True when a dashboard under the stable ``url_path`` exists
after the call (pre-existing or created here), False when the
Lovelace dashboards collection was unavailable or refused. A
dashboard already registered under the stable ``url_path`` — whether
ours from a previous setup or a user's own — is left COMPLETELY
alone: never overwritten, never deleted, never a duplicate.
"""
collection = _dashboards_collection(hass)
if collection is None:
_LOGGER.warning(
"Lovelace dashboards collection unavailable; the NestQuest "
"panel dashboard was not registered. Enable the Lovelace "
"integration in storage mode or create a dashboard with "
"strategy %s manually.",
DASHBOARD_STRATEGY_TYPE,
)
return False
try:
items = await collection.async_items()
if _existing_dashboard(items):
return True
await collection.async_create_item(
{
"url_path": DASHBOARD_URL_PATH,
"title": DASHBOARD_TITLE,
"icon": DASHBOARD_ICON,
"show_in_sidebar": True,
"mode": "storage",
"strategy": {"type": DASHBOARD_STRATEGY_TYPE},
}
)
except Exception:
_LOGGER.warning(
"Registering the NestQuest panel dashboard in the Lovelace "
"dashboards collection failed; set it up manually with "
"strategy %s.",
DASHBOARD_STRATEGY_TYPE,
exc_info=True,
)
return False
return True