`Europe/London` sur un banc français était une erreur de configuration. La correction a été différée tant que le décalage n'était pas visible : déplacer les frontières d'une heure sans rien qui le dise aurait rendu l'historique illisible de part et d'autre, sans qu'aucun lecteur ne puisse savoir où passait la couture. `periodStartedAt`, posé hier, a levé cette réserve. COUTURE : 2026-09-05T06:53:30Z. Avant, journée de reporting de 23:00 à 23:00 UTC ; après, de 22:00 à 22:00. Toute comparaison jour à jour qui franchit cet instant compare deux découpages différents. Méthode : `Configuration.SetTimeZone` en JSON-RPC. `System.SetTimeZone` N'EXISTE PAS en 1.15.2 — malgré la note de dépréciation de `Configuration.SetTimeZone` qui dit d'utiliser celui-là. Suivre la note aurait donné « No such method ». CONFIRMÉ EN VRAI : le moteur a suivi SANS redémarrage de nymead. Ce n'était jusqu'ici mesuré qu'en conteneur avec un programme Qt6 ; `GetEnergyRatios.timeZone` et `System.GetTime.timeZone` sont passés à Europe/Paris au cycle suivant, et `periodStartedAt` a reculé de 23:00 à 22:00 UTC. ET LA PAIRE A SERVI AU MOMENT EXACT OÙ ELLE SERVAIT. `measuredSince` n'a pas bougé (05:04:29 UTC) : la frontière publiée a changé, la portée réelle de la mesure non. Sans le second champ, le bilan du jour aurait paru couvrir une heure de plus qu'il ne couvre — un décalage introduit par la correction elle-même, le lendemain du jour où on a posé de quoi le voir.
178 lines
11 KiB
Markdown
178 lines
11 KiB
Markdown
# Fuseau horaire — le silence d'une heure
|
|
|
|
> Ouvert le 2026-09-04, après qu'une obligation éco déclarée à `"12:00"` se soit résolue
|
|
> à 13:00 sur l'horloge de l'atelier. Le moteur avait raison ; c'est la question qui
|
|
> n'avait jamais été posée.
|
|
|
|
## 1. Le constat
|
|
|
|
`.75` tourne en `Europe/London`. Une obligation `dailyDeadline: "12:00"` s'y résout donc à
|
|
12:00 heure de Londres, soit **13:00 en France**. Rien, nulle part, ne le dit.
|
|
|
|
L'écart est d'une heure et ressemble trait pour trait à un défaut d'heure d'été dans la
|
|
résolution d'échéance — j'ai failli l'imputer au moteur avant de lire l'horloge de la box.
|
|
C'est le premier coût du silence : il déplace le soupçon.
|
|
|
|
Le second coût est celui qui compte. Chez un client français, une box mal fuseau-horée décale
|
|
**toutes** ses fenêtres temporelles. Une obligation « avant 7 h » se déclenche à 8 h. Et
|
|
quand les heures creuses, Tempo et le marché de gros arriveront, elles se décaleront aussi —
|
|
sauf que là, personne ne le verra avant de compter sa facture.
|
|
|
|
## 2. Fait établi — il n'y a qu'UN fuseau, et il vient du système
|
|
|
|
Vérifié, pas supposé :
|
|
|
|
- `EnergyArbitrator::update()` prend `QDateTime::currentDateTime()`
|
|
(`energyplugin/etm/energyarbitrator.cpp:329`), donc le fuseau du PROCESSUS `nymead`.
|
|
- `resolveDeadline()` construit l'échéance avec `now.timeZone()`
|
|
(`energyplugin/etm/types/ecotarget.cpp`). Elle n'introduit aucun fuseau à elle : elle
|
|
hérite.
|
|
- nymea n'a pas de fuseau propre. `System.GetTime` renvoie `QTimeZone::systemTimeZoneId()`
|
|
(`libnymea-core/jsonrpc/systemhandler.cpp`), et `System.SetTimeZone` écrit le fuseau
|
|
**système** via le `SystemController` de la plateforme. `Configuration.SetTimeZone` est
|
|
déprécié au profit du premier.
|
|
|
|
**Conséquence : la box et le moteur ne peuvent pas diverger entre eux.** Il n'existe pas de
|
|
réglage nymea qui dirait autre chose que `timedatectl`. C'était la première question — elle
|
|
est fermée, et dans le sens rassurant.
|
|
|
|
## 3. Fait mesuré — un processus vivant SUIT le changement
|
|
|
|
Mesuré le 2026-09-04 dans le conteneur `build-cross-arm64`, avec un programme Qt6 qui lit
|
|
`QTimeZone::systemTimeZoneId()` et `QDateTime::currentDateTime()` en boucle pendant qu'on
|
|
bascule `/etc/localtime` sous lui :
|
|
|
|
```
|
|
systemTimeZoneId=Europe/London currentDateTime=06:48:50 BST zone(now)=Europe/London
|
|
--- /etc/localtime bascule vers Europe/Paris ---
|
|
systemTimeZoneId=Europe/Paris currentDateTime=07:48:53 CEST zone(now)=Europe/Paris
|
|
```
|
|
|
|
**Changer le fuseau ne demande donc PAS de redémarrer `nymead`.** Ce n'est pas une évidence :
|
|
la mise en cache du fuseau par processus est un comportement classique de la libc, et si Qt
|
|
l'avait fait, une correction de fuseau serait restée sans effet jusqu'au prochain
|
|
redéploiement — une correction qui ne corrige rien étant pire que pas de correction.
|
|
|
|
Corollaire à ne pas perdre : une échéance en cours de résolution peut donc **changer d'heure
|
|
sous le moteur**, sans redémarrage et sans notification. Cf. §6, étape 3.
|
|
|
|
## 4. La vraie divergence n'est pas là où on la cherchait
|
|
|
|
Box et moteur s'accordent par construction. Ce qui peut diverger, c'est :
|
|
|
|
**(a) L'installation et le client.** `dailyDeadline` est une chaîne `"HH:mm"` **sans fuseau**.
|
|
Le client saisit « 7:00 » dans l'app, sur un téléphone qui est dans SON fuseau, en pensant
|
|
7:00 chez lui. Le moteur la lit dans le fuseau de la box. Si les deux diffèrent, l'écart est
|
|
silencieux **des deux côtés** — l'app affiche ce qu'elle a saisi, le moteur exécute autre
|
|
chose, et les deux ont raison dans leur référentiel.
|
|
|
|
Un fuseau implicite est une absence qui signifie « le défaut s'applique ». C'est exactement
|
|
ce qu'on s'interdit ailleurs (`measurement.source`, `rankOrigin`, `progress.regime`).
|
|
|
|
**(b) L'installation et le CALENDRIER RÉGLEMENTÉ.** Et c'est le point neuf.
|
|
|
|
## 5. La distinction qui décide : deux natures d'heure murale
|
|
|
|
Elles ne prennent pas leur fuseau au même endroit, et les confondre est le piège.
|
|
|
|
| | nature | fuseau qui fait foi | qui le vérifie |
|
|
|---|---|---|---|
|
|
| `dailyDeadline`, « avant 7 h » | **habitude du client** | celui de l'installation | l'app, à la saisie |
|
|
| heures creuses, Tempo, EPEX | **calendrier réglementé** | celui où le tarif est DÉFINI | `LM-1005-b`, au lot tarifaire |
|
|
| taux journaliers (« minuit local ») | **période de reporting** | celui de l'installation | **personne** |
|
|
|
|
La troisième s'est trouvée en cherchant autre chose : `GetEnergyRatios` dérive ses Δ **depuis
|
|
minuit local** (`docs/INTERFACE_energyratios.md` §2). Elle suit l'installation, ce qui est le bon
|
|
comportement — mais elle repose sur la même prémisse non vérifiée que la première : que le fuseau
|
|
de la box SOIT celui de l'installation. Sur `.75`, le bilan journalier de l'app se remet à zéro à
|
|
01:00 heure française, et rien nulle part ne le dit.
|
|
|
|
**Ce que la troisième nature ajoute au raisonnement** : elle montre que la prémisse « la box est
|
|
bien réglée » ne porte pas seulement une saisie, elle porte des **grandeurs publiées**. Une
|
|
échéance mal fuseau-horée se corrige d'un réglage ; un bilan journalier découpé au mauvais
|
|
endroit a déjà été lu, affiché, et peut-être facturé.
|
|
|
|
Une plage d'heures creuses française est définie en `Europe/Paris` par le fournisseur, pas
|
|
« à l'heure locale de la box ». Une box française mal réglée ne décale pas seulement
|
|
l'affichage : elle **achète au mauvais moment**, et le tarif, lui, ne s'est pas décalé.
|
|
|
|
Aujourd'hui les deux natures prendraient silencieusement le fuseau de la box. Pour la
|
|
première c'est correct **si** la box est bien réglée ; pour la seconde c'est faux dès qu'elle
|
|
ne l'est pas. Une grandeur réglementée ne doit donc pas hériter du fuseau système : elle doit
|
|
porter le sien.
|
|
|
|
Le marché de gros amont travaille en instants ABSOLUS (les tests construisent leurs
|
|
horodatages via `EnergyTestBase::utcDateTime`), donc il est déjà à l'abri. **Le risque naît le
|
|
jour où l'on écrira une plage tarifaire en heures murales — et pas un jour avant.**
|
|
|
|
> **La règle opposable ne vit donc PAS ici.** Elle est écrite là où le risque naît :
|
|
> `specs/spec_loadmodel.md`, **LM-1005-b**, en précondition de livraison du lot tarifaire —
|
|
> au même titre que le délestage était précondition du §10. Une distinction rangée dans le
|
|
> document qui l'a découverte est une note ; rangée dans le lot qu'elle contraint, c'est une
|
|
> condition d'entrée. Ce document n'en garde que l'établissement des faits.
|
|
|
|
## 6. La décision : ni imposer, ni refuser — DÉCLARER
|
|
|
|
**Ne pas imposer.** Le moteur ne connaît pas le fuseau « vrai » de l'installation ; il ne
|
|
connaît que celui de la box. Le forcer à `Europe/Paris` serait poser une affirmation qu'on ne
|
|
sait pas vérifier, et serait faux par construction pour toute box vendue hors de France.
|
|
|
|
**Ne pas refuser de fonctionner.** Un HEMS qui s'arrête parce qu'un téléphone est en
|
|
itinérance laisse un chauffe-eau sans pilotage et une protection de disjoncteur sans arbitre.
|
|
**Les couches L0 à L4 ne doivent dépendre d'aucun fuseau** : elles raisonnent en durées, pas
|
|
en heures murales. Le refus est disproportionné, et il déplacerait un problème d'affichage
|
|
vers la sécurité.
|
|
|
|
**Déclarer**, donc — la seule réponse conforme à la doctrine maison : *publier le régime, ne
|
|
jamais le laisser déduire.* Le moteur dit dans quel fuseau il résout les heures murales ;
|
|
c'est au client de rapprocher cela de l'heure de son utilisateur, et de l'avertir.
|
|
|
|
### Les étapes, à comportement constant d'abord
|
|
|
|
1. **Documenter le contrat** (aucun changement de code). `dailyDeadline` est une heure murale
|
|
du fuseau de la box, tel que publié par `System.GetTime.timeZone`. Écrit dans
|
|
`INTERFACE.md`, à côté du champ.
|
|
2. **Publier le fuseau avec la configuration**, en lecture seule, plutôt que d'exiger du
|
|
client qu'il croise deux espaces de noms pour interpréter un de NOS champs. Un client qui
|
|
doit appeler `System.GetTime` pour donner un sens à `dailyDeadline` finira par ne pas le
|
|
faire.
|
|
3. **Dire le changement.** Le fuseau peut bouger sous le moteur sans redémarrage (§3). Une
|
|
échéance résolue à 12:00 hier peut valoir 13:00 aujourd'hui sans qu'aucun champ n'ait
|
|
changé. À traiter comme tout élément runtime : un `Get*` ET un `*Changed`.
|
|
4. **Séparer les deux natures** — porté par **LM-1005-b**, précondition du lot tarifaire, et
|
|
non par ce lot-ci. Rien à faire ici : la condition est posée à l'entrée de l'autre.
|
|
5. **Côté app** (lot séparé, à elle) : comparer le fuseau de la box à celui de l'appareil au
|
|
moment de la SAISIE d'une échéance, et avertir. C'est l'écran de saisie qui doit parler,
|
|
pas l'écran de résultat — au moment du résultat, l'heure est déjà passée.
|
|
|
|
### Le fuseau de `.75` — corrigé le 2026-09-05, et la date compte
|
|
|
|
`Europe/London` sur un banc français était une erreur de configuration. La correction a été
|
|
**différée** tant que le décalage n'était pas visible : la déplacer d'une heure sans rien qui le
|
|
dise aurait rendu l'historique illisible de part et d'autre, sans qu'aucun lecteur ne puisse
|
|
savoir où passait la couture. `periodStartedAt` a levé cette réserve — le décalage se lit
|
|
désormais dans la charge utile.
|
|
|
|
**LA COUTURE, à retenir pour toute lecture de l'historique énergie de `.75` :**
|
|
|
|
| | |
|
|
|---|---|
|
|
| **Instant du changement** | `2026-09-05T06:53:30Z` — soit 07:53:30 heure box AVANT, 08:53:30 APRÈS |
|
|
| **Avant** | `Europe/London` — journée de reporting de 23:00 UTC à 23:00 UTC |
|
|
| **Après** | `Europe/Paris` — journée de reporting de 22:00 UTC à 22:00 UTC |
|
|
| **Méthode** | `Configuration.SetTimeZone` en JSON-RPC (`System.SetTimeZone` n'existe pas en 1.15.2, malgré ce que dit sa propre note de dépréciation) |
|
|
|
|
**Tout enregistrement antérieur à cet instant a ses frontières de journée une heure plus tard
|
|
que les suivants.** Une comparaison jour à jour qui franchit la couture compare deux découpages
|
|
différents.
|
|
|
|
**Ce que la correction a confirmé, en vrai.** Le moteur a suivi **sans redémarrage de `nymead`** —
|
|
ce qui n'était jusque-là mesuré qu'en conteneur (§3). `GetEnergyRatios.timeZone` et
|
|
`System.GetTime.timeZone` sont passés à `Europe/Paris` au cycle suivant, et `periodStartedAt` a
|
|
reculé de 23:00 à 22:00 UTC.
|
|
|
|
**Et la paire a joué son rôle au moment exact où elle servait.** `measuredSince` n'a PAS bougé
|
|
(05:04:29 UTC, le redémarrage du déploiement de la veille) : la frontière publiée a changé, la
|
|
portée réelle de la mesure non. Sans le second champ, le bilan du jour aurait paru couvrir une
|
|
heure de plus qu'il ne couvre — un décalage introduit par la correction elle-même.
|