The burned-in OSD timestamps on the telep-mainframe cameras were wrong on both camera devices — the ÉSZAK pair (right column of the camwall) by 109 days, the DÉL pair (left column) by exactly 1 hour. Root cause: the cameras sit on the isolated cams VLAN with no working NTP, and the Tapo firmware ignores the ONVIF TimeZone and DaylightSavings fields. Fixed with cam-timesync.timer on the host, which pushes host time over ONVIF hourly with a DST-aware offset compensation.
For Agents
- Scope: OSD only. This is the timestamp painted into the video by the camera. Frigate stamps its own recordings/events from host time, so recording timelines, event times and alerts were always correct. Only the pixels lied.
- The tell:
curl -s -o /tmp/x.jpg 'http://127.0.0.1:5000/api/telep_cam3/latest.jpg'and read the top-left corner. Compare todate.- The fix is installed and automatic:
cam-timesync.timer(hourly,Persistent=true,OnBootSec=3min). Manual run:sudo /usr/local/bin/cam-timesync.py(add--forceto push even when skew ≤ 60s).- Do NOT try to fix this by setting the ONVIF timezone. Tapo firmware accepts the SOAP call, returns HTTP 200, and silently keeps its own TZ. Verified twice.
Symptoms
| Camwall pane | Camera | Device | OSD read | Actual | Error |
|---|---|---|---|---|---|
| top/bottom left | telep_cam1 / telep_cam2 (DÉL) | 192.168.30.119 | 2026-08-09 00:47 | 2026-08-08 23:47 | +1 h |
| top/bottom right | telep_cam3 / telep_cam4 (ÉSZAK) | 192.168.30.139 | 2026-04-21 04:32 | 2026-08-08 23:47 | −109 d |
Pane→camera mapping per 2026-07-28-camwall-4-substream-composite (DÉL = left column, ÉSZAK = right column). Both physical devices are dual-lens Tapos, so one broken device = two broken panes.
Diagnosis
Unauthenticated GetSystemDateAndTime (ONVIF allows this one without WS-Security) on port 2020 tells you everything:
curl -s -X POST "http://192.168.30.139:2020/onvif/device_service" \
-H 'Content-Type: application/soap+xml; charset=utf-8' \
-d '<?xml version="1.0"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body><GetSystemDateAndTime xmlns="http://www.onvif.org/ver10/device/wsdl"/></s:Body></s:Envelope>'.139(ÉSZAK):DateTimeType=NTP,DaylightSavings=true, clock stuck at2026-04-21. AuthenticatedGetNTPreturnedFromDHCP=true,IPv4Address=0.0.0.0→ the camera has no NTP server address at all. It claims NTP mode and syncs against nothing, so its RTC has been free-running since ~21 April..119(DÉL):DateTimeType=Manual,DaylightSavings=false, and itsUTCDateTimefield held local CEST, not UTC. WithTZ=GMT-01:00on top, the OSD landed an hour ahead.
Host time itself was fine (timedatectl: synchronized, Europe/Budapest), as was the Frigate container’s (/etc/localtime bind-mounted, TZ=Europe/Budapest).
Why NTP never worked
Camera VLAN (telep-cc) documents the design as “NTP is DNAT’d back to the router, so a camera with no internet still keeps its clock.” The DNAT rule exists, but DHCP option 42 is not being served on the cams VLAN, so the cameras never learned an NTP server IP to send packets to in the first place. A DNAT of traffic that is never generated does nothing. (192.168.30.1:123 also did not answer an NTP probe from the LAN side, though that may be interface-scoped and is not conclusive.)
Gotchas
Tapo silently ignores the ONVIF
TimeZoneandDaylightSavingsfields
SetSystemDateAndTimereturns HTTP 200 with a cleanSetSystemDateAndTimeResponseand then keepsTZ=GMT-01:00regardless of what you sent — tried both a full POSIX string (CET-1CEST,M3.5.0/2,M10.5.0/3) and the device’s ownGMT-02:00format.DaylightSavings=trueis likewise not applied to the local-time computation. Only the date/time numbers inUTCDateTimeare writable. A 200 here means “message parsed”, not “setting applied” — always read back withGetSystemDateAndTimeand compareLocalDateTimetodate.
The ONVIF
TZstring is POSIX-signed, soGMT-01:00means UTC +1Confirmed empirically on this hardware: with
TZ=GMT-01:00the device reportsLocalDateTime = UTCDateTime + 1 h. Do not read that string as “one hour behind GMT”.cam-timesync.pyparses the sign and derives the real offset rather than assuming.
DateTimeType=NTPdoes not mean the clock is synced
.139self-reported NTP mode the entire time it was 109 days out. The mode field is aspiration, not status. Always checkGetNTPfor an actual server address —0.0.0.0+FromDHCP=trueis the “never syncs” signature.
The fix — cam-timesync
Because the timezone is unwritable and NTP is unreachable, the host pushes pre-compensated time: read the camera’s own TZ offset, subtract it from host local time, and write the result into UTCDateTime so the camera’s (wrong but fixed) TZ arithmetic lands on the correct local time.
Files installed on telep-mainframe:
| Path | Role |
|---|---|
/usr/local/bin/cam-timesync.py | ONVIF WS-Security digest client; reads state, pushes compensated time, reads back to verify |
/etc/cam-timesync.env | FRIGATE_CAM{,2}_{USER,PASSWORD}, mode 0600 |
/etc/systemd/system/cam-timesync.service | oneshot, After=time-sync.target |
/etc/systemd/system/cam-timesync.timer | OnBootSec=3min, OnUnitActiveSec=1h, Persistent=true, enabled |
Behaviour: skips any camera already within 60 s (so the hourly run is a cheap no-op), otherwise pushes and re-reads, logging corrected -3601s -> 2026-08-08 23:49:13 (skew -0s). Non-zero exit if a camera can’t be reached or won’t take the correction.
It survives the DST changeover on its own
The compensation is recomputed every run from
host local offsetvscamera TZ offset, both read live. On 25 Oct 2026 the host drops to UTC+1, the camera staysGMT-01:00, the compensation goes to zero, and the OSD stays correct with no intervention. This is why the offset is derived rather than hardcoded to 1 h.
.139 was moved from DateTimeType=NTP to Manual as part of this — deliberate, since its NTP was decorative and Manual is what the push maintains.
Verified: fresh latest.jpg from both telep_cam1 and telep_cam3 read 2026-08-08 23:49:48 against a host clock of 23:49:48.
Open follow-up — the root cause is router-side
The host-side push fully fixes the symptom and is robust to reboots, drift and DST, but the actual gap is that the cams VLAN never hands out an NTP server. The clean fix on telep-router is to serve DHCP option 42:
uci set dhcp.cams.dhcp_option='42,192.168.30.1'
uci commit dhcp && /etc/init.d/dnsmasq restart
…and confirm OpenWrt’s NTP server is actually listening on the cams interface. Not done here — router changes carry real blast radius on this box (2026-08-03-telep-router-factory-reset-recovery) and the timer already covers it. If option 42 is fixed later, cam-timesync can stay as a harmless belt-and-braces check (it no-ops when skew ≤ 60 s), though the Tapo TZ bug means the +1 h DST error would come back under pure NTP — so keep the timer regardless.
Related
- 2026-07-28-camwall-4-substream-composite — pane→camera mapping
- 2026-08-03-cam-stall-recovery-and-casino-alert — the other ONVIF-driven camera repair (reboot a wedged camera)
- Camera VLAN (telep-cc) — the isolated VLAN and its NTP DNAT
- telep-mainframe