Repository navigation
Republish Tuya mesh status when the wall clock steps - #256
Conversation
last_seen is derived from the monotonic clock plus the time offset, so a bulb heard before nerves_time set the clock kept a stale date until another event rebuilt the status. The Resolver now monitors time-offset changes and republishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The test’s recurring timer can mask a missing :CHANGE handler or race with status flushing and cause timeouts, so it does not reliably verify notification-triggered publication.
1 open issue
What changed in this PR
Updates the Tuya mesh Resolver to refresh wall-clock last_seen timestamps after clock adjustments, correcting stale values shown in the Bluetooth page and Home Assistant after boot.
Changes:
- Monitors Erlang time-offset changes and republishes updated mesh status.
- Adds an injectable time-offset function for testing.
- Adds a regression test simulating a 71-day clock step.
| File | Description |
|---|---|
test/universal_proxy/tuya_mesh/supervisor_test.exs |
Tests timestamp correction and status publication after a simulated clock step. |
lib/universal_proxy/tuya_mesh/resolver.ex |
Handles time-offset notifications and makes timestamp conversion testable. |
Review effort: Balanced
The periodic tick also republishes, so the test could pass without the handler. It now starts with a far-off tick, waits on the broadcast, and drains before changing the offset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ERTS notices an OS clock step only some seconds after it happens, so System.time_offset stayed pre-step while DateTime.utc_now already showed the new date. A shared WallClock helper derives the offset from os_time minus monotonic_time, used by the Resolver and MeshSync; the 5 s tick republish now corrects last_seen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Stateless offset rounding can turn boundary jitter into alternating last_seen values, triggering unnecessary broadcasts and Home Assistant updates without a new report or clock step.
1 open issue
1 resolved since last review
Review effort: Balanced
Rounding each sample independently could flip last_seen by a second when the raw offset jittered across a rounding boundary. WallClock now keeps the accepted offset in persistent_term and adopts a new reading only when it differs by more than 1 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The remaining Pi reboot check must validate hardware-specific OS/ERTS clock divergence and confirm Bluetooth UI and Home Assistant timestamps recover within one tick after the actual nerves_time clock step.
No open issues 🎉
1 resolved since last review
Review effort: Balanced

Summary
Found on hardware: the Pi boots with its clock at the system image's build date (2026-08-23), and nerves_time steps it forward shortly after boot. A boot trace (polled every 2 s) showed the OS clock already correct by 17 s of uptime. The Resolver's
last_seen, though, stayed on August until about 62 s. ERTS updatesSystem.time_offsetonly when it detects the step, about 45 s later, andlast_seenwas converted with that offset. The Bluetooth page showed "Seen 71 d ago" for about a minute after every boot, and HA's "Last seen" sensor got the same wrong time.UniversalProxy.TuyaMesh.WallClockconverts monotonic to wall clock withSystem.os_time - System.monotonic_time, the same clockDateTime.utc_now/0uses. The offset is rounded to whole seconds so read jitter can't cause republishes. The Resolver (last_seen) and MeshSync (live_of, for the HA timestamp sensor) both use it, so the 5 s tick correctslast_seenwithin one tick of the step.:erlang.monitor(:time_offset, :clock_service)) and republishes on them. That covers an idle mesh, where nothing ticks.:time_offset_funoption makes the conversion testable.Test plan
mise run test: 2399 passed. New: a tick-only test (the OS offset steps and the next tick republishes the correctedlast_seen), a:CHANGE-only test (periodic publishing disabled; fails with the handler removed), andWallClockunit tests.compile --warnings-as-errors,credo --strict, dialyzer, Argus (0 new)last_seenis never more than one tick behind the OS clock🤖 Generated with Claude Code