PQC257 — Set the device clock from the wall clock, never a re-projected local time¶
GetLocalNow().LocalDateTimere-projects the device wall clock onto the host process zone.
SynchronizeTime(TimeProvider timeProvider) receives a zone-aware provider whose local zone is the device's configured zone. timeProvider.GetLocalNow() therefore returns a DateTimeOffset whose components already are the device wall clock — 11:22 +02:00.
.LocalDateTime throws that away. It re-projects the same instant onto the zone of the host process, which in a container is UTC, and hands you 09:22.
// reported
var localNow = DateTime.SpecifyKind(timeProvider.GetLocalNow().LocalDateTime, DateTimeKind.Unspecified);
return Protocol.SetTime(localNow);
// ^^^^^^^^ two hours behind the device
Why the build refuses this¶
The panel has no timezone. It is a wall-clock device: whatever you send is what it shows and what it stamps events with. Setting it from a re-projected time puts it off by the container-to-device offset permanently.
The failure is silent. It compiles, the sync reports success, and nothing looks wrong until someone reads an audit log whose timestamps are hours off — usually long after the incident they were needed for.
It desynchronises the two halves of the clock. The zone used to set the clock must be the zone used to read event wall clocks back; EventBuilderExtensions.At resolves that from the same Timezone property. Converting on the way out breaks the pair.
How to fix it¶
Keep the offset — do not convert it. Type the protocol-layer set-clock method DateTimeOffset and read its components, which are already in the device zone:
// Thing
public Task SynchronizeTime(TimeProvider timeProvider) =>
Protocol.SetClock(timeProvider.GetLocalNow()); // straight through
// Protocol — components are the device wall clock
public Task SetClock(DateTimeOffset time) =>
SendClockFrame(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second);
DateTimeOffset.DateTime is not the alternative — it is banned repo-wide because it drops Kind, and it would only move the same mistake one property over.
What is not reported¶
Reading components. timeProvider.GetLocalNow().Hour, .Day, .Offset — these are the device wall clock and the whole point of the zone-aware provider.
.LocalDateTime on an offset from somewhere else. A device timestamp parsed off the wire is not this rule's subject; only the direct conversion of a provider call is reported.
An offset that travelled through a variable first. The check covers the direct chain only. The rule still applies — review catches the rest, and so does the surrounding guidance in RULE-057: any zone math between GetLocalNow() and the wire is a violation, including TimeZoneInfo.ConvertTimeFromUtc and rebuilding a DateTime from components.
If you disagree with a report¶
Do not suppress it. A wrong report is a bug in the check — report it with the code that triggered it.