diff --git a/homeassistant/components/bluetooth/active_update_coordinator.py b/homeassistant/components/bluetooth/active_update_coordinator.py index 37f049d3e0724e..ab26a0260f38ac 100644 --- a/homeassistant/components/bluetooth/active_update_coordinator.py +++ b/homeassistant/components/bluetooth/active_update_coordinator.py @@ -3,13 +3,13 @@ from collections.abc import Callable, Coroutine import logging -import time from typing import Any, Generic, TypeVar from bleak import BleakError from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.debounce import Debouncer +from homeassistant.util.dt import monotonic_time_coarse from . import BluetoothChange, BluetoothScanningMode, BluetoothServiceInfoBleak from .passive_update_processor import PassiveBluetoothProcessorCoordinator @@ -94,7 +94,7 @@ def needs_poll(self, service_info: BluetoothServiceInfoBleak) -> bool: """Return true if time to try and poll.""" poll_age: float | None = None if self._last_poll: - poll_age = time.monotonic() - self._last_poll + poll_age = monotonic_time_coarse() - self._last_poll return self._needs_poll_method(service_info, poll_age) async def _async_poll_data( @@ -124,7 +124,7 @@ async def _async_poll(self) -> None: self.last_poll_successful = False return finally: - self._last_poll = time.monotonic() + self._last_poll = monotonic_time_coarse() if not self.last_poll_successful: self.logger.debug("%s: Polling recovered") diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index aaefd3dcfc4c44..c3a0e0998f1919 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -7,7 +7,6 @@ from datetime import datetime, timedelta import itertools import logging -import time from typing import TYPE_CHECKING, Any, Final from bleak.backends.scanner import AdvertisementDataCallback @@ -22,6 +21,7 @@ ) from homeassistant.helpers import discovery_flow from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from .advertisement_tracker import AdvertisementTracker from .const import ( @@ -69,7 +69,7 @@ APPLE_DEVICE_ID_START_BYTE, } -MONOTONIC_TIME: Final = time.monotonic +MONOTONIC_TIME: Final = monotonic_time_coarse _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/bluetooth/scanner.py b/homeassistant/components/bluetooth/scanner.py index fe795f7ace556f..6b23cae02183e7 100644 --- a/homeassistant/components/bluetooth/scanner.py +++ b/homeassistant/components/bluetooth/scanner.py @@ -6,7 +6,6 @@ from datetime import datetime import logging import platform -import time from typing import Any import async_timeout @@ -22,6 +21,7 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse from homeassistant.util.package import is_docker_env from .const import ( @@ -35,7 +35,7 @@ from .util import adapter_human_name, async_reset_adapter OriginalBleakScanner = bleak.BleakScanner -MONOTONIC_TIME = time.monotonic +MONOTONIC_TIME = monotonic_time_coarse # or_patterns is a workaround for the fact that passive scanning # needs at least one matcher to be set. The below matcher diff --git a/homeassistant/components/bluetooth/util.py b/homeassistant/components/bluetooth/util.py index 860428a6106efb..181796d3d2d300 100644 --- a/homeassistant/components/bluetooth/util.py +++ b/homeassistant/components/bluetooth/util.py @@ -2,11 +2,11 @@ from __future__ import annotations import platform -import time from bluetooth_auto_recovery import recover_adapter from homeassistant.core import callback +from homeassistant.util.dt import monotonic_time_coarse from .const import ( DEFAULT_ADAPTER_BY_PLATFORM, @@ -29,7 +29,7 @@ async def async_load_history_from_system() -> dict[str, BluetoothServiceInfoBlea bluez_dbus = BlueZDBusObjects() await bluez_dbus.load() - now = time.monotonic() + now = monotonic_time_coarse() return { address: BluetoothServiceInfoBleak( name=history.advertisement_data.local_name diff --git a/homeassistant/components/esphome/bluetooth/scanner.py b/homeassistant/components/esphome/bluetooth/scanner.py index 284e605fdfa5fd..7c8064d5583d04 100644 --- a/homeassistant/components/esphome/bluetooth/scanner.py +++ b/homeassistant/components/esphome/bluetooth/scanner.py @@ -19,6 +19,7 @@ ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.dt import monotonic_time_coarse TWO_CHAR = re.compile("..") @@ -84,7 +85,7 @@ def discovered_devices_and_advertisement_data( @callback def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None: """Call the registered callback.""" - now = time.monotonic() + now = monotonic_time_coarse() address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper name = adv.name if prev_discovery := self._discovered_device_advertisement_datas.get(address): diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index 80b322c1a14c8f..44e4403d689dd5 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -4,7 +4,9 @@ import bisect from contextlib import suppress import datetime as dt +import platform import re +import time from typing import Any import zoneinfo @@ -13,6 +15,7 @@ DATE_STR_FORMAT = "%Y-%m-%d" UTC = dt.timezone.utc DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc +CLOCK_MONOTONIC_COARSE = 6 # EPOCHORDINAL is not exposed as a constant # https://github.com/python/cpython/blob/3.10/Lib/zoneinfo/_zoneinfo.py#L12 @@ -461,3 +464,26 @@ def _datetime_ambiguous(dattim: dt.datetime) -> bool: assert dattim.tzinfo is not None opposite_fold = dattim.replace(fold=not dattim.fold) return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() + + +def __monotonic_time_coarse() -> float: + """Return a monotonic time in seconds. + + This is the coarse version of time_monotonic, which is faster but less accurate. + + Since many arm64 and 32-bit platforms don't support VDSO with time.monotonic + because of errata, we can't rely on the kernel to provide a fast + monotonic time. + + https://lore.kernel.org/lkml/20170404171826.25030-1-marc.zyngier@arm.com/ + """ + return time.clock_gettime(CLOCK_MONOTONIC_COARSE) + + +monotonic_time_coarse = time.monotonic +with suppress(Exception): + if ( + platform.system() == "Linux" + and abs(time.monotonic() - __monotonic_time_coarse()) < 1 + ): + monotonic_time_coarse = __monotonic_time_coarse diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 79cd4e5e0dfdea..e902176bb35471 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import datetime, timedelta +import time import pytest @@ -719,3 +720,8 @@ def test_find_next_time_expression_tenth_second_pattern_does_not_drift_entering_ assert (next_target - prev_target).total_seconds() == 60 assert next_target.second == 10 prev_target = next_target + + +def test_monotonic_time_coarse(): + """Test monotonic time coarse.""" + assert abs(time.monotonic() - dt_util.monotonic_time_coarse()) < 1