Propagate timezone setting to host in OS 16.2 and newer (#6099)

* Propagate timezone setting to host in OS 16.2 and newer

With home-assistant/operating-system#4224, timezone setting in OS can be
peristently set in HAOS as well. Propagate the timezone configured in
Supervisor config (which can be changed through general system settings
in HA Core) through the DBus API for setting the timezone.

* Persist timezone also when it's been obtained from Whoami

* Suppress pylint fixme error
This commit is contained in:
Jan Čermák
2025-08-20 01:30:57 +02:00
committed by GitHub
parent 4109c15a36
commit b49ce96df8
7 changed files with 112 additions and 1 deletions

View File

@@ -142,6 +142,7 @@ class APISupervisor(CoreSysAttributes):
):
await self.sys_run_in_executor(validate_timezone, timezone)
await self.sys_config.set_timezone(timezone)
await self.sys_host.control.set_timezone(timezone)
if ATTR_CHANNEL in body:
self.sys_updater.channel = body[ATTR_CHANNEL]

View File

@@ -392,6 +392,19 @@ class Core(CoreSysAttributes):
async def _adjust_system_datetime(self) -> None:
"""Adjust system time/date on startup."""
# Ensure host system timezone matches supervisor timezone configuration
if (
self.sys_config.timezone
and self.sys_host.info.timezone != self.sys_config.timezone
and self.sys_dbus.timedate.is_connected
):
_LOGGER.info(
"Timezone in Supervisor config '%s' differs from host '%s'",
self.sys_config.timezone,
self.sys_host.info.timezone,
)
await self.sys_host.control.set_timezone(self.sys_config.timezone)
# If no timezone is detect or set
# If we are not connected or time sync
if (
@@ -413,7 +426,9 @@ class Core(CoreSysAttributes):
_LOGGER.warning("Can't adjust Time/Date settings: %s", err)
return
await self.sys_config.set_timezone(self.sys_config.timezone or data.timezone)
timezone = self.sys_config.timezone or data.timezone
await self.sys_config.set_timezone(timezone)
await self.sys_host.control.set_timezone(timezone)
# Calculate if system time is out of sync
delta = data.dt_utc - utcnow()

View File

@@ -112,3 +112,8 @@ class TimeDate(DBusInterfaceProxy):
async def set_ntp(self, use_ntp: bool) -> None:
"""Turn NTP on or off."""
await self.connected_dbus.call("set_ntp", use_ntp, False)
@dbus_connected
async def set_timezone(self, timezone: str) -> None:
"""Set timezone on host."""
await self.connected_dbus.call("set_timezone", timezone, False)

View File

@@ -3,6 +3,8 @@
from datetime import datetime
import logging
from awesomeversion import AwesomeVersion
from ..const import HostFeature
from ..coresys import CoreSysAttributes
from ..exceptions import HostNotSupportedError
@@ -80,3 +82,24 @@ class SystemControl(CoreSysAttributes):
_LOGGER.info("Setting new host datetime: %s", new_time.isoformat())
await self.sys_dbus.timedate.set_time(new_time)
await self.sys_dbus.timedate.update()
async def set_timezone(self, timezone: str) -> None:
"""Set timezone on host."""
self._check_dbus(HostFeature.TIMEDATE)
# /etc/localtime is not writable on OS older than 16.2
if (
self.coresys.os.available
and self.coresys.os.version is not None
and self.sys_os.version >= AwesomeVersion("16.2.dev0")
):
_LOGGER.info("Setting host timezone: %s", timezone)
await self.sys_dbus.timedate.set_timezone(timezone)
await self.sys_dbus.timedate.update()
else:
# pylint: disable=fixme
# TODO: we can change this to a warning once 16.2 is out
_LOGGER.info(
"Skipping persistent timezone setting, OS %s < 16.2",
self.sys_os.version,
)

View File

@@ -82,6 +82,24 @@ async def test_dbus_setntp(
assert timedate.ntp is False
async def test_dbus_set_timezone(
timedate_service: TimeDateService, dbus_session_bus: MessageBus
):
"""Test setting of host timezone."""
timedate_service.SetTimezone.calls.clear()
timedate = TimeDate()
with pytest.raises(DBusNotConnectedError):
await timedate.set_timezone("Europe/Prague")
await timedate.connect(dbus_session_bus)
assert await timedate.set_timezone("Europe/Prague") is None
assert timedate_service.SetTimezone.calls == [("Europe/Prague", False)]
await timedate_service.ping()
assert timedate.timezone == "Europe/Prague"
async def test_dbus_timedate_connect_error(
dbus_session_bus: MessageBus, caplog: pytest.LogCaptureFixture
):

View File

@@ -1,9 +1,12 @@
"""Test host control."""
import pytest
from supervisor.coresys import CoreSys
from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.hostname import Hostname as HostnameService
from tests.dbus_service_mocks.timedate import TimeDate as TimeDateService
async def test_set_hostname(
@@ -20,3 +23,33 @@ async def test_set_hostname(
assert hostname_service.SetStaticHostname.calls == [("test", False)]
await hostname_service.ping()
assert coresys.dbus.hostname.hostname == "test"
@pytest.mark.parametrize("os_available", ["16.2"], indirect=True)
async def test_set_timezone(
coresys: CoreSys,
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
os_available: str,
):
"""Test set timezone."""
timedate_service: TimeDateService = all_dbus_services["timedate"]
timedate_service.SetTimezone.calls.clear()
assert coresys.dbus.timedate.timezone == "Etc/UTC"
await coresys.host.control.set_timezone("Europe/Prague")
assert timedate_service.SetTimezone.calls == [("Europe/Prague", False)]
@pytest.mark.parametrize("os_available", ["16.1"], indirect=True)
async def test_set_timezone_unsupported(
coresys: CoreSys,
all_dbus_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
os_available: str,
):
"""Test DBus call is not made when OS doesn't support it."""
timedate_service: TimeDateService = all_dbus_services["timedate"]
timedate_service.SetTimezone.calls.clear()
await coresys.host.control.set_timezone("Europe/Prague")
assert timedate_service.SetTimezone.calls == []

View File

@@ -83,6 +83,7 @@ async def test_adjust_system_datetime_if_time_behind(
side_effect=[WhoamiData("Europe/Zurich", utc_ts)],
) as mock_retrieve_whoami,
patch.object(SystemControl, "set_datetime") as mock_set_datetime,
patch.object(SystemControl, "set_timezone") as mock_set_timezone,
patch.object(
InfoCenter, "dt_synchronized", new=PropertyMock(return_value=False)
),
@@ -92,6 +93,21 @@ async def test_adjust_system_datetime_if_time_behind(
mock_retrieve_whoami.assert_called_once()
mock_set_datetime.assert_called_once()
mock_check_connectivity.assert_called_once()
mock_set_timezone.assert_called_once_with("Europe/Zurich")
async def test_adjust_system_datetime_sync_timezone_to_host(
coresys: CoreSys, websession: MagicMock
):
"""Test _adjust_system_datetime method syncs timezone to host when different."""
await coresys.core.sys_config.set_timezone("Europe/Prague")
with (
patch.object(SystemControl, "set_timezone") as mock_set_timezone,
patch.object(InfoCenter, "timezone", new=PropertyMock(return_value="Etc/UTC")),
):
await coresys.core._adjust_system_datetime()
mock_set_timezone.assert_called_once_with("Europe/Prague")
async def test_write_state_failure(