La distinction des deux natures d'heure murale déménage du lot qui l'a découverte vers le lot qu'elle contraint. Rangée dans DESIGN_FUSEAU.md, c'était une note ; rangée à l'entrée du lot tarifaire, c'est une condition d'entrée — au même titre que l'extension du délestage l'était pour le §10. LM-1005-b, posée juste sous LM-1005, là où la spec nomme déjà la frontière que l'arbitre franchit en devenant acheteur : « à quel prix, à quelle heure, sous quel signal tarifaire ». C'est exactement là que le risque naît, et pas un jour avant — tant que l'arbitre distribue un surplus, il n'a aucune heure à respecter. · `dailyDeadline` (habitude du client) → fuseau de l'installation · heures creuses, Tempo, EPEX (calendrier réglementé) → fuseau où le tarif est DÉFINI Une plage d'heures creuses française est définie en Europe/Paris par le fournisseur, pas « à l'heure locale de la box ». Les confondre ne décale pas un affichage : ça achète au mauvais moment, et le tarif ne s'est pas décalé. La facture est le seul endroit où ça se verrait, un mois trop tard. Le marché de gros amont travaille en instants absolus, donc il est déjà à l'abri. Le risque apparaît à la première plage écrite en heures murales. DESIGN_FUSEAU.md ne garde que l'établissement des faits et renvoie ici pour la règle opposable — pas de règle en double, un seul domicile. Note de forme, qui a coûté une passe : l'index des règles lit `l.strip()`, donc une définition placée dans une citation `>` n'est JAMAIS reconnue — elle sort en « citée et jamais définie ». L'énoncé vit hors de la citation, le bloc d'appui dedans.
141 lines
8.0 KiB
Markdown
141 lines
8.0 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 |
|
|
|---|---|---|
|
|
| `dailyDeadline`, « avant 7 h » | **habitude du client** | celui de l'installation |
|
|
| heures creuses, Tempo, EPEX | **calendrier réglementé** | celui où le tarif est DÉFINI |
|
|
|
|
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.
|
|
|
|
### Ce qui reste ouvert, et qu'on ne tranche pas ici
|
|
|
|
Le fuseau de `.75` lui-même. `Europe/London` sur un banc français est une erreur de
|
|
configuration, mais la corriger déplace d'une heure la lecture de tout l'historique
|
|
d'énergie déjà enregistré. C'est une décision d'exploitation, pas de conception.
|