# 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.