/// **Lire un INSTANT publié par la box** — et refuser de deviner son fuseau. /// /// Le contrat porte désormais la règle : *tout horodatage publié est en UTC avec son /// marqueur, ou en epoch*. Ce fichier existe parce qu'elle n'a pas toujours été portée, et /// que l'écart ne se voyait pas. /// /// ## Ce que Dart fait, et pourquoi c'est le piège /// /// ``` /// DateTime.parse('2026-09-06T20:19:04') -> isUtc = false, .hour = 20, toLocal().hour = 20 /// DateTime.parse('2026-09-06T20:19:04Z') -> isUtc = true, .hour = 20, toLocal().hour = 22 /// ``` /// /// Une chaîne **sans marqueur** rend un `DateTime` que Dart tient pour local. `toLocal()` /// n'a alors **aucun effet** — il ne convertit que ce qui est marqué UTC. Une valeur qui /// était pourtant de l'UTC se retrouve donc décalée du fuseau de l'appareil, sans erreur, /// sans trace, et dans les deux sens selon l'endroit du monde où on la lit. /// /// Deux défauts frères, trouvés le même jour par la même bascule : côté app, `measuredSince` /// rendu en heure UTC ; côté moteur, `command.since` publié sans marqueur. /// /// ⚠️ Ce que la bascule a donné, c'est le **motif de regarder**, pas la visibilité. L'écart /// se joue entre UTC et l'**appareil** : il est visible sur tout téléphone hors UTC+0, quel /// que soit le fuseau de la box. Vérifié le 2026-09-07 avec les deux en `Europe/Paris` — /// `04:33:28Z` publié, « 04:33 » à l'écran, 06:33 attendu. Croire que le fuseau de la box y /// est pour quelque chose mène à « les fuseaux coïncident, donc c'est réglé », et laisse le /// défaut en place. /// /// ## La politique retenue pour une chaîne sans marqueur /// /// On la lit comme de l'**UTC**, et ce n'est pas une supposition : c'est la forme /// qu'une box antérieure au contrat publiait, **mesurée** avant sa correction — de l'UTC /// écrit sans le dire. Comme le contrat interdit désormais la forme nue, l'ensemble des /// chaînes non marquées est clos : ce sont les anciennes box, et elles sont en UTC. /// /// ⚠️ Cette politique vaut pour ce que **la box publie**, et pour rien d'autre. Les dates /// que l'app écrit elle-même (`Installation.lastSeen`) sont sérialisées en local sans /// marqueur et relues pareil : les lire comme de l'UTC les décalerait. Deux héritages /// opposés sous une même forme — c'est précisément pourquoi il n'y a **pas** de « parser une /// date » unique dans ce dépôt, et pourquoi celui-ci nomme sa source dans son nom. library; /// L'instant publié par la box, **rendu dans le fuseau du lecteur**. /// /// Accepte l'epoch en secondes (`num`) et la chaîne ISO, marquée ou non. Rend `null` sur /// tout le reste — un champ absent est un état, pas un zéro. /// /// Un INSTANT se rend toujours chez le lecteur ; c'est un **réglage** comme /// `needs.dailyDeadline` qui se rend dans l'horloge de la box, et qui doit alors la nommer. DateTime? instantDeLaBox(Object? v) { if (v is num) { return DateTime.fromMillisecondsSinceEpoch(v.toInt() * 1000, isUtc: true) .toLocal(); } if (v is! String || v.isEmpty) return null; final d = DateTime.tryParse(v); if (d == null) return null; // `isUtc == false` ⇒ la chaîne ne portait pas de marqueur. On la requalifie en UTC au // lieu de laisser Dart la tenir pour locale : voir la politique en tête de fichier. final utc = d.isUtc ? d : DateTime.utc(d.year, d.month, d.day, d.hour, d.minute, d.second, d.millisecond, d.microsecond); return utc.toLocal(); }