Skip to content

Stabilise the uptime sensor timestamp between polls - #148

Open
shauneccles wants to merge 2 commits into
HarvsG:masterfrom
shauneccles:fix/uptime-recompute-noise
Open

Stabilise the uptime sensor timestamp between polls#148
shauneccles wants to merge 2 commits into
HarvsG:masterfrom
shauneccles:fix/uptime-recompute-noise

Conversation

@shauneccles

Copy link
Copy Markdown

Summary

The Uptime sensor's boot timestamp drifts and flaps instead of staying constant, producing a stream of spurious state changes (and recorder/history churn). This recomputes it stably, aligning the implementation with Home Assistant core's UniFi integration.

Root cause

The router exposes uptime as a seconds counter, so the sensor derives the boot time as now - uptime. The old native_value recomputed this live on every property read:

self._current_value = _uptime_calculation(
    self.router.system_status["uptime"], self._current_value
)

But HA polls the sensor on its own schedule, decoupled from the router's 30 s system_status refresh (router.py: async_track_time_interval(self.update_states, SCAN_INTERVAL)). Between refreshes the uptime value is frozen while utcnow() keeps advancing, so now - uptime drifts forward on each read. Once the drift exceeds the 15 s deviation tolerance it re-anchors — and whenever that jump lands in a different minute bucket, the displayed timestamp visibly changes.

Evidence (from my live HA instance)

sensor.gl_inet_mt6000_uptime — the router has been continuously up since 2026-06-15 ~06:04 UTC, so the boot timestamp should be a flat line. Instead (all times UTC):

Flapping across a minute boundary — the same two values alternate every poll:

poll (last_updated) reported boot time
2026-06-22 05:04:50 06:03:46
2026-06-22 05:05:20 06:04:16
2026-06-22 05:42:50 06:03:46
2026-06-22 05:43:20 06:04:16
2026-06-22 06:02:50 06:03:46
2026-06-22 06:03:50 06:04:16

Continuous ramp — when system_status goes stale (e.g. the router is briefly unreachable, which also shows up in the logs below) the frozen uptime + advancing utcnow() makes the boot time climb 30 s on every 30 s poll:

poll (last_updated) reported boot time
2026-06-23 02:14:04 06:04:46
2026-06-23 02:14:34 06:05:16
2026-06-23 02:15:04 06:05:46
2026-06-23 02:15:34 06:06:16
… (every poll) …
2026-06-23 02:48:04 06:25:46

Over those ~34 minutes the reported boot time advanced ~21 minutes — while the router never rebooted. Every row above is a separate state change persisted to the recorder.

Corresponding router-unreachable window in home-assistant.log (the trigger for the stale-data ramp):

2026-06-23 13:00:46 WARNING [custom_components.glinet.router] Could not connect to GL-iNet router to renew token: Cannot connect to host 192.168.1.1:80
2026-06-23 13:01:11 INFO    [custom_components.glinet.router] GL-iNet router http://192.168.1.1 token was renewed
2026-06-23 13:01:11 INFO    [custom_components.glinet.router] Reconnected to Gl-inet router http://192.168.1.1

Fix

  • Recompute the boot timestamp only when the router reports a fresh uptime value — between polls the cached _attr_native_value is returned unchanged, so it can no longer drift against the clock.
  • Keep a deviation tolerance to absorb second-granularity / poll jitter, structured as a pure derive function plus a "changed?" gate.
  • Tolerance raised 15 s → 120 s and named (UPTIME_DEVIATION), and switched to the dt_util import convention.
@property
def native_value(self) -> datetime | None:
    uptime = self.router.system_status["uptime"]
    if uptime != self._last_uptime:
        self._last_uptime = uptime
        candidate = _derive_boot_time(uptime)
        if _boot_time_changed(self._attr_native_value, candidate):
            self._attr_native_value = candidate
    return self._attr_native_value

Alignment with core HA patterns

This mirrors how core handles derived uptime timestamps:

  • UniFi uses the same now - uptime derivation plus a value_changed_fn tolerance gate (120 s) so the timestamp doesn't flap — unifi/sensor.py (async_client_uptime_value_fn / async_uptime_value_changed_fn). The freshness UniFi gets from event-driven pushes, glinet's polled model gets from the uptime != self._last_uptime guard.
  • System Monitor caches boot time once ("Boot time only needs to refresh on first pass") — systemmonitor/coordinator.py.
  • The Uptime integration sets the value once at startup — docs.

Test plan

  • ruff check and py_compile pass.
  • Verified the boot timestamp stays constant across polls and stale-data windows; only re-anchors on a genuine reboot or > 120 s clock divergence.

🤖 Generated with Claude Code

The router reports uptime as a seconds counter, so the boot timestamp is
derived as `now - uptime`. native_value recomputed this live on every
read, but Home Assistant polls the sensor on its own schedule, decoupled
from the router's 30s system-status refresh. Between refreshes the uptime
value is frozen while utcnow() advances, so the derived boot time drifts
forward on each read until it crosses the 15s deviation tolerance and
re-anchors -- emitting a stream of spurious state changes whenever the
jump crosses a minute boundary.

Recompute only when the router reports a fresh uptime value, and gate the
committed timestamp with a deviation tolerance, mirroring core's UniFi
integration (value_fn + value_changed_fn, 120s). Switch to dt_util and
store the result in _attr_native_value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant