Adopt Home Assistant's time zone for the API household clock (task 6351b36d) (#275)

Co-authored-by: Joshua <joshua@cnjmail.com>
Co-committed-by: Joshua <joshua@cnjmail.com>
This commit was merged in pull request #275.
This commit is contained in:
2026-09-28 23:04:16 -04:00
committed by approver_agent
parent 07f2eec3bf
commit 40d0e5d8f6
11 changed files with 774 additions and 58 deletions
+14 -7
View File
@@ -113,18 +113,25 @@ CONF_END_OF_DAY_REPORT_ENABLED = "end_of_day_report_enabled"
CONF_CELEBRATION_ENABLED = "celebration_enabled"
DEFAULT_AUTOMATION_ENABLED = True
# The household's IANA time zone (e.g. ``America/New_York``) the API
# service reads its clock in. The empty string keeps the historical
# behaviour — the API host's own local time — so an existing install is
# unchanged until the owner sets it. The integration itself always uses
# Home Assistant's configured ``time_zone`` and ignores this field.
# service reads its clock in. The empty string is the API host's own
# local time. While the admin has not chosen a zone
# (``timezone_configured`` is ``False``) the API adopts the zone the
# integration reports from Home Assistant's configured ``time_zone``
# (:data:`PANEL_TIMEZONE_HEADER`), so the default is the household's
# zone without any manual setup. The integration itself always uses
# Home Assistant's ``time_zone`` and ignores this field.
CONF_TIMEZONE = "timezone"
DEFAULT_TIMEZONE = ""
# Whether the admin has made an explicit ``timezone`` choice. ``False``
# (a fresh install) lets the admin UI auto-set the browser's zone once;
# ``True`` pins the stored value — including an explicitly-chosen empty
# ``timezone`` (the API host's local time) — so it is never auto-set again.
# (a fresh install) lets the API follow Home Assistant's reported zone
# and the admin UI auto-set the browser's zone once while none is
# stored; ``True`` pins the stored value — including an explicitly-chosen
# empty ``timezone`` (the API host's local time) — so neither changes it.
CONF_TIMEZONE_CONFIGURED = "timezone_configured"
DEFAULT_TIMEZONE_CONFIGURED = False
# The panel-plane request header carrying Home Assistant's configured
# IANA ``time_zone`` from the integration to the API service.
PANEL_TIMEZONE_HEADER = "X-NestQuest-Timezone"
# Panel-plane API client (Feature 18): the integration reaches the
# NestQuest API service (Feature 16) over HTTP instead of the database.
+7 -5
View File
@@ -327,12 +327,14 @@ class NestQuestSettings:
def household_now(self, instant: datetime.datetime) -> datetime.datetime:
"""Return the aware ``instant`` expressed in the household's zone.
The API service has no Home Assistant ``time_zone``, so its one
The API service does not run inside Home Assistant, so its one
clock read (an aware host-local ``now``) is converted here: with
:attr:`timezone` set the SAME instant is re-expressed in that
zone, so ``.date()`` and ``.time()`` are the household's wall
clock even when the host runs UTC. With :attr:`timezone` empty
the instant is returned unchanged (the host's local time).
:attr:`timezone` set — the admin's choice, or else the zone the
integration reports from Home Assistant — the SAME instant is
re-expressed in that zone, so ``.date()`` and ``.time()`` are the
household's wall clock even when the host runs UTC. With
:attr:`timezone` empty the instant is returned unchanged (the
host's local time).
"""
if not self.timezone:
return instant
@@ -36,7 +36,8 @@ after the first update:
this field existed simply lacks it and resolves to ``""``,
- ``timezone_configured`` — a real boolean: ``True`` once the admin
explicitly chose ``timezone`` (including the empty host-local value),
so the admin UI never auto-sets it again. A document stored before
so neither Home Assistant's reported zone
(:func:`adopt_reported_timezone`) nor the admin UI changes it again. A document stored before
this field existed lacks it and resolves to ``False``.
Only values whose core validators ACCEPTED are ever written, so a
@@ -200,18 +201,49 @@ async def update_settings(
)
async with _settings_lock(database):
current = await load_settings(database)
document: dict[str, Any] = {
field: getattr(current, field) for field in _FIELD_NAMES
}
for name, raw_value in changes.items():
default, validator = _RESOLUTIONS[name]
try:
document[name] = validator(
default if raw_value is None else raw_value
)
except ValueError as error:
raise ValueError(_named(name, error)) from error
await MetaStateDao(database).set(
SETTINGS_META_KEY, json.dumps(document, sort_keys=True)
)
return await _write_merged(database, current, changes)
async def adopt_reported_timezone(
database: NestQuestDatabase, zone: str
) -> NestQuestSettings:
"""Adopt Home Assistant's reported ``zone`` unless the admin chose one.
While ``timezone_configured`` is ``False`` the stored ``timezone``
follows the zone the integration reports, so the API's clock is the
household's without any manual setup; ``timezone_configured`` stays
``False`` (the value is automatic, not an admin choice). Once the
admin has chosen — including an explicitly-chosen empty zone — the
stored settings are returned untouched. The check and the write
share the settings lock, so a concurrent admin choice is never
overwritten. An invalid ``zone`` raises a ``ValueError`` naming
``timezone`` and writes nothing.
"""
async with _settings_lock(database):
current = await load_settings(database)
if current.timezone_configured or current.timezone == zone:
return current
return await _write_merged(database, current, {"timezone": zone})
async def _write_merged(
database: NestQuestDatabase,
current: NestQuestSettings,
changes: Mapping[str, Any],
) -> NestQuestSettings:
"""Validate ``changes``, merge onto ``current``, persist; lock held."""
document: dict[str, Any] = {
field: getattr(current, field) for field in _FIELD_NAMES
}
for name, raw_value in changes.items():
default, validator = _RESOLUTIONS[name]
try:
document[name] = validator(
default if raw_value is None else raw_value
)
except ValueError as error:
raise ValueError(_named(name, error)) from error
await MetaStateDao(database).set(
SETTINGS_META_KEY, json.dumps(document, sort_keys=True)
)
return NestQuestSettings(**document)