Adopt Home Assistant's time zone for the API household clock
The deployed API host runs UTC and the household `timezone` setting was never set, so the panel snapshot read UTC: morning quests showed overdue hours early and `today_iso` rolled over at UTC midnight. The integration now reports `hass.config.time_zone` on every panel request (`X-NestQuest-Timezone`). While the admin has not chosen a zone (`timezone_configured` is false) the API adopts and persists it, so the panel, the admin plane, and the missed-sweep scheduler all read the household clock out of the box. An explicit admin choice — including an explicitly chosen empty zone — still wins; an invalid reported zone is logged and ignored. No schema change. Task: 6351b36d-0eef-4e7d-bf5a-96b754cbc883 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+38
-10
@@ -9,8 +9,9 @@ this router inherits the check).
|
||||
The snapshot route is a thin adapter over the bundled core: it performs
|
||||
NO business logic. It reads the live database off ``app.state.db``,
|
||||
resolves ONE timezone-aware ``now`` (the host clock re-expressed in the
|
||||
stored household ``timezone`` setting — the API host may run UTC while
|
||||
the household does not), calls the pure
|
||||
household ``timezone`` — the API host may run UTC while the household
|
||||
does not; until the admin chooses a zone it is the Home Assistant zone
|
||||
the integration reports in :data:`PANEL_TIMEZONE_HEADER`), calls the pure
|
||||
:func:`nestquest_core.snapshot.build_snapshot`, shapes each child's
|
||||
instances through :func:`nestquest_core.snapshot.instance_payload` with
|
||||
``include_missed=False`` (D-009: missed instances are OMITTED from the
|
||||
@@ -53,6 +54,7 @@ from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import importlib
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
@@ -62,6 +64,7 @@ from api.database import DatabaseState
|
||||
from api.dependencies import require_panel_token
|
||||
from api.nestquest_core import (
|
||||
core_completion,
|
||||
core_const,
|
||||
core_events,
|
||||
core_settings_store,
|
||||
core_snapshot,
|
||||
@@ -69,6 +72,12 @@ from api.nestquest_core import (
|
||||
from api import transitions as api_transitions
|
||||
from api.sse import transition_event_stream
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
#: The request header the integration reports Home Assistant's
|
||||
#: configured IANA ``time_zone`` in.
|
||||
PANEL_TIMEZONE_HEADER = core_const.PANEL_TIMEZONE_HEADER
|
||||
|
||||
#: The instance DAO, imported on the app's core copy for the
|
||||
#: child-mismatch guard on the complete route.
|
||||
_dao_instances = importlib.import_module("nestquest_core.dao_instances")
|
||||
@@ -179,16 +188,35 @@ def _local_now() -> datetime.datetime:
|
||||
|
||||
|
||||
async def _household_clock(
|
||||
database: object,
|
||||
database: object, request: Request
|
||||
) -> tuple[object, datetime.datetime]:
|
||||
"""Return the stored settings and ONE household-local clock read.
|
||||
"""Return the settings and ONE household-local clock read.
|
||||
|
||||
The API host may run UTC while the household does not, so the one
|
||||
:func:`_local_now` read is re-expressed in the stored settings'
|
||||
``timezone`` (:meth:`NestQuestSettings.household_now`); an unset
|
||||
zone keeps the host's local time.
|
||||
:func:`_local_now` read is re-expressed in the settings' ``timezone``
|
||||
(:meth:`NestQuestSettings.household_now`). The integration reports
|
||||
Home Assistant's ``time_zone`` in :data:`PANEL_TIMEZONE_HEADER`;
|
||||
until the admin chooses a zone it is adopted and persisted
|
||||
(:func:`nestquest_core.settings_store.adopt_reported_timezone`), so
|
||||
the scheduler and the admin plane follow it too. An invalid
|
||||
reported zone is logged and ignored — the panel keeps serving on
|
||||
the stored settings.
|
||||
"""
|
||||
settings = await core_settings_store.load_settings(database)
|
||||
reported = request.headers.get(PANEL_TIMEZONE_HEADER, "").strip()
|
||||
if reported:
|
||||
try:
|
||||
settings = await core_settings_store.adopt_reported_timezone(
|
||||
database, reported
|
||||
)
|
||||
except ValueError:
|
||||
LOGGER.warning(
|
||||
"Ignoring invalid %s header value %r",
|
||||
PANEL_TIMEZONE_HEADER,
|
||||
reported,
|
||||
)
|
||||
settings = await core_settings_store.load_settings(database)
|
||||
else:
|
||||
settings = await core_settings_store.load_settings(database)
|
||||
return settings, settings.household_now(_local_now())
|
||||
|
||||
|
||||
@@ -210,7 +238,7 @@ async def panel_snapshot(request: Request) -> PanelSnapshotResponse:
|
||||
:class:`PanelSnapshotResponse`.
|
||||
"""
|
||||
state: DatabaseState = request.app.state.db
|
||||
settings, now = await _household_clock(state.database)
|
||||
settings, now = await _household_clock(state.database, request)
|
||||
snapshot = await core_snapshot.build_snapshot(
|
||||
state.database, settings, now
|
||||
)
|
||||
@@ -297,7 +325,7 @@ async def panel_complete_instance(
|
||||
detail="Quest instance not found for this child",
|
||||
)
|
||||
|
||||
_settings, now = await _household_clock(database)
|
||||
_settings, now = await _household_clock(database, request)
|
||||
try:
|
||||
result = await core_completion.complete_instance(
|
||||
database,
|
||||
|
||||
Reference in New Issue
Block a user