427 Commits

Author SHA1 Message Date
Patrick Schurig
3084b885d8 test : reproductions du lot délestage V2C (rouges sur b054780)
Mock : classe chargerUnmetered, une borne qui mesure sa puissance mais
publie ses courants sous currentL1..3 (comme le plugin V2C Trydan), jamais
sous currentPhaseX. EvCharger::meteredPhases() y est vide ; aucune borne du
mock ne produisait ce cas.

- testOffPeakUnmeteredChargerSettlesUnderThePhaseLimit : heures creuses,
  limite 25 A, marge 10 %, borne non mesurée (mono sur A, tri) ; rouge :
  16, 6, 13, 6, 13… A, et 6 A tenus avec le lave-vaisselle.
- testUnmeteredChargerGivesNothingBackWhenUnsure : triphasée avec voiture
  monophasée, courants par phase publiés et nuls ; vert (témoin).
- testWaterfallChargerSettlesUnderThePhaseLimit (question 5) : hors heures
  creuses ; rouge : 16, 9, 16, 9… A (maison légère), 16 A puis coupée par
  L4 à 26 A pour 25 (maison chargée).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 14:16:36 +01:00
Patrick Schurig
b0547806d8 decisions : référence régénérée (lot délestage triphasé)
Six fonctions nouvelles. Hors elles, 94 lignes changent dans 8 fonctions, toutes expliquées :
- seuil à 22,5 A pour 25 A (marge de 10 %) : draw.authorisedW −575 W, dépassements et marges
  DRAW_CAP au seuil (testSheddingLogKeepsTheLastEpisodes, testSheddingLogCoversEveryMechanism,
  testL4ShedsAPacAndSaysItForcedTheLock, testWorstPhaseBindsWhenPhasesAreUnbalanced) ;
- entrées de test déplacées pour la marge, et thresholdA publié
  (testOverloadProtectionReasonOnTheCharger, testOverloadProtectionOnAManualChargerIsLogged) ;
- testBlockedDischargeShowsAtTheMeterAndL4Decides : le plafond d'heures creuses descend avec le
  seuil, la borne passe de 16 à 6 A ;
- testOffPeakFullPowerStaysUnderTheLimitOnABlindMeter (marge nulle) : phases aveugles à la borne,
  relevées au niveau du total ; consignes inchangées.
run et toutes les autres fonctions : identiques.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:44:41 +01:00
Patrick Schurig
8eb2f7b00b moteur : la cohérence avec le total ignore un manque sous le demi-watt
Un manque de 10⁻¹² W, arrondi de la somme des phases, changeait des décisions au douzième
chiffre dans le scénario run de la preuve decisions, sans rien protéger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:18:46 +01:00
Patrick Schurig
3dfb74dcb3 test : la marge de sécurité dans les tests existants
- Tests de l'algorithme amont (charging, simulation::run) et famille « heures creuses à pleine
  puissance » : marge nulle, posée explicitement au point d'appel. Ils figent une arithmétique
  contre la limite ; la marge déplace le seuil sans toucher à ce calcul.
- testWorstPhaseBindsWhenPhasesAreUnbalanced : marge et plafond au seuil (22,5 A pour 25 A).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 13:12:15 +01:00
Patrick Schurig
5ab5213aad moteur : la marge de soutirage borne aussi les consignes d'un optimiseur (défauts 4 et 5)
La marge que le compteur laisse sous le seuil ne bornait que la cascade du rule-based. Sous un
optimiseur qui demande le maximum, ce que la protection venait de retirer revenait au cycle
suivant, dès que le compteur repassait sous la limite : 32,2 A pendant une minute sur
l'installation d'essai, après un délestage du chauffe-eau.

bornerOptimiseurAuSoutirage(), avant le dispatch : une consigne d'optimiseur ne fait monter une
charge que de la marge libre (pire phase, consommée dans l'ordre du créneau). Elle ne réduit
rien — c'est la protection qui réduit. L'engagé d'une borne est sa dernière commande tenue, si
bien que le dispatch ne défait plus, dans le même cycle, ce que la protection vient de faire.
Une consigne écrêtée publie DRAW_CAP et clamped, plus le motif de l'optimiseur (défaut 5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:48:30 +01:00
Patrick Schurig
440a45f526 moteur : marge de sécurité de 10 % et phases cohérentes avec le total (défaut 2)
Décision du 2026-10-27 : le moteur vise 90 % de la limite de phase — 27 A pour 30 A — pour le
plafond qu'il déclare et alloue, et pour le seuil où la protection agit. Réglable de 0 à 25 %,
persistée avec la limite (phaseSafetyMarginPercent), publiée par GetPhasePowerLimit et
PhasePowerLimitChanged (même charge utile : limite, marge, effectivePhaseLimit), écrite par le
paramètre optionnel de SetPhasePowerLimit. Le seuil atteint DrawCap, la protection amont et sa
reprise, la limite de prepareInformation(), les deux allowances du proxy, et le phaseLimitA
envoyé à l'optimiseur. OVERLOAD_THROTTLED porte thresholdA quand la marge n'est pas nulle.

Défaut 2 : sur l'installation d'essai, les courants par phase publiés étaient en retard sur le
total (29,1 A lus pour 32,3 A importés par phase) et la protection a attendu 1 min 45. Les phases
somment au total : la plus chargée en porte au moins le tiers. tiragesCoherentsAvecLeTotal()
relève les phases quand leur somme est sous le total, jamais ne les abaisse ; c'est la lecture de
la protection, de sa reprise, du journal de ses gestes et de la cascade.

Tests existants mis à jour : les dépassements se mesurent au seuil (22,5 A pour 25 A).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:43:20 +01:00
Patrick Schurig
cf0e65abc7 moteur : une borne non préparée ce cycle n'a pas de ChargingProcessInfo (défaut 1)
internalHasProcessInfo() testait la table m_processInfos, qui n'est jamais vidée et que
verifyOverloadProtection() remplit par operator[] (entrée par défaut : 1 phase, pas de bascule).
Après la première surcharge, une borne triphasée sans voiture assignée devenait monophasée pour
evTargetPhases() : plafond 16 × 230 × 1, conversion W → A sur une phase, 3 680 W alloués et
attendus pour 11 040 W tirés. Même effet, sans surcharge, avec l'entrée d'une voiture
monophasée désassignée.

Le manager retient désormais les bornes qu'il prépare à chaque cycle (m_preparedChargers), et
l'accesseur dit ce que sa documentation promettait : préparée CE cycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:32:57 +01:00
Patrick Schurig
f20b08a3a2 test : reproductions du lot délestage triphasé (rouges sur 1.15.2+etm76)
- testOverloadLeavesTheChargerItsPhases (3 cas) et testAnUnassignedCarTakesItsPhasesAway :
  une borne triphasée sans voiture, ou dont la voiture monophasée est désassignée, comptée sur
  une phase — 3 680 W alloués et attendus pour 16 A × 3.
- testOverloadReactsToTheTotalWhenPhasesLag : des courants par phase en retard sur le total
  retardent la protection.
- testPhaseSafetyMargin : la marge de 10 % (décision du 2026-10-27).
- testTheDrawCapHoldsAgainstAnOptimizerAtMaximum (refoptimizer) : sous un optimiseur qui
  demande le maximum, ce que la protection a retiré revient au cycle suivant.
- Témoins verts sur etm76 : testSheddingOpensTheRelaysUnderMinOn (le délestage franchit minOn
  et ouvre les relais) et testCommandDivergenceSeesAPhaseCountError (×3 vu en régime établi).
- Optimiseur de référence : mode over-limit (toute consigne en watts à 50 kW).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 12:28:42 +01:00
Patrick Schurig
2d06de0bc1 debian : entrée de changelog 1.15.2+etm76
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-25 20:07:21 +01:00
Patrick Schurig
c75cc32470 fusion : fix/recredit-oscillation (écart commande/mesure jugé au début du cycle, borne qui retient ce qu'elle tient)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-25 20:07:08 +01:00
Patrick Schurig
0a88a0f97f commentaires : la marge de progression renvoie au document de conception
La justification de ProgressionMinW cite la négociation la plus lente
par son document (DESIGN_COMMANDE_PERDUE.md §9.1) plutôt que comme un
relevé. Dans les tests, « banc » désignait le scénario : mot remplacé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:58:17 +01:00
Patrick Schurig
f9f2e1e5c5 decisions : référence régénérée (verdict de l'écart commande/mesure)
Contre la référence d'etm75, 29 fonctions changent, toutes par le
verdict de l'écart commande/mesure ou ce qui en découle :
- 21 par `command.divergent` seul (marches suivies et premières
  commandes qui ne divergent plus ; mesures remises à la main par le
  harnais qui divergent) ;
- `run` : `expectedW` de la borne commandée par le proxy (mémoire de
  commande tous émetteurs) et 46 cycles de gel sous arrêt verrouillé ;
- 7 où la garde de refus mord enfin : datée à l'horloge murale, elle
  était inerte dans toute la suite de simulation.
Plus les 6 fonctions de test nouvelles. Détail dans le rapport du lot.

run-decisions.sh check : IDENTIQUE (24 415 lignes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:52:02 +01:00
Patrick Schurig
c9e4a251ca test : le rejeu de borne part d'une date fixe
rejouerBorne() partait de l'horloge murale, comme le test de
reproduction devait le faire tant que l'écart était daté à l'horloge
murale. Le moteur n'en dépend plus, et l'enregistreur `decisions` date
les cycles : la référence n'était pas reproductible à la milliseconde
près. Mêmes chiffres de rejeu avec une date fixe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:40:11 +01:00
Patrick Schurig
e856c48084 test : un vrai refus reste vu, sur la borne et sur les autres charges
testARealRefusalIsStillSeen — borne figée sous une consigne à la hausse :
refus au cycle qui suit la commande, comme avant le correctif ; montée
qui progresse puis cale : latence tant qu'elle se rapproche, refus au
cycle où elle cale ; descente suivie de 500 W seulement : refus au cycle
suivant, la progression n'excuse jamais une descente.

testWattLoadsFollowedOrRefusedUnderAMeter — relais et consigne continue
sous compteur dédié : marche suivie dans les deux sens sans refus ; refus
réel vu au deuxième cycle (la latence par défaut de 60 s n'est pas
dépassée par un cycle de 60 s) ; sans compteur, rien ne change.

testSgReadyDivergenceIsPublishedOnly — sous compteur dédié, une PAC qui
monte en minutes n'est pas divergente, une PAC qui ne démarre pas l'est ;
son recrédit (puissance déclarée) n'en dépend pas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:31:05 +01:00
Patrick Schurig
c4e517a8d9 borne : retenir ce que la borne tient, et geler l'arrêt verrouillé (P2)
La borne retenait la consigne VOULUE par son adaptateur, et seulement
quand le waterfall la commandait. Deux faux refus en sortaient :
- une coupure refusée par le verrou de marche/arrêt (300 s) laissait la
  borne à 6 A pendant qu'on attendait 0 W ;
- une borne rabaissée par la protection L4, ou commandée par le proxy
  (échéance, spot), restait jugée contre la dernière consigne du
  waterfall.

La mémoire de commande est posée par EnergyArbitrator::
executeChargingAction(), point unique de tous les émetteurs, avec ce qui
a PASSÉ les verrous et l'instant de la commande.

Pendant que l'arrêt est verrouillé, la borne déclare lockMinPowerW =
courant minimal × 230 × phases, comme un relais sous minOn : le
waterfall gèle ces watts (LOCK_MIN_ON) au lieu de les prêter.

Le verrou borne la SOMME éco + confort : la part confort n'est relevée
que de ce que la passe éco ne couvre pas déjà. Sans cela, une borne
servie à 6 A en éco sous arrêt verrouillé recevait 12 A (trouvé par la
preuve decisions, aucun test ne le voyait).

Tests : testLockedStopFreezesTheMinimumAndIsNoRefusal,
testALockedStopDoesNotAddToTheEcoFloor, et une garde L4 dans
testOverloadProtectionReasonOnTheCharger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:31:00 +01:00
Patrick Schurig
0dc2a1aeba moteur : l'écart commande/mesure est jugé au début du cycle (P1)
L'écart était jugé APRÈS le dispatch, entre la consigne qui venait de
partir et la mesure de la précédente, puis consommé au cycle suivant
avant d'être rejugé. Toute marche de consigne suivie par la borne se
lisait donc comme un refus vieux d'un cycle (60 s, au-delà des 30 s /
7 s de la borne) : recrédit suspendu, coupure, et une boucle de trois
cycles sous un surplus constant (~1,5 kW exportés en moyenne).

jugerCommande() rend désormais UN jugement par cycle, au début du cycle,
lu par le recrédit et par la télémétrie :
- la commande EN VIGUEUR contre la mesure fraîche ; s'ils concordent
  (max(200 W, 15 %)), l'épisode se referme ;
- l'ouverture est datée à l'instant de la commande, en temps de cycle,
  et non plus à l'horloge murale de la dernière publication ;
- le sens est celui que la commande demande à la charge, et non plus
  deviné au niveau de la mesure ;
- en montée seulement, une mesure rapprochée de la commande de plus de
  200 W depuis le cycle précédent est une latence. La descente n'a
  jamais cette excuse.

command.divergent publie ce verdict ; expectedW et observedW restent
les nombres de la publication.

Le test de reproduction inverse ses assertions « DÉFAUT » : sur trois
rampes (instantanée, 30 s / 7 s, 120 s), 13 A tenus dès le premier
cycle, aucune suspension, export de 10 W dès que la borne a rejoint sa
consigne.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 19:30:45 +01:00
Patrick Schurig
41ce86a634 test : une marche de consigne suivie fait suspendre le recrédit (reproduction)
Borne monophasée 6-16 A, 3 000 W de surplus constant. L'écart commande/mesure
est jugé après le dispatch, entre la consigne qui vient de partir et la mesure
de la précédente, puis consommé au cycle suivant par qualifierRefus() avant
d'être rejugé. Une marche suivie se lit donc comme un refus vieux d'un cycle,
au-delà des 30 s / 7 s de la borne : recrédit retenu, coupure (bloquée par le
verrou de 300 s, 6 A tirés pour 0 attendu), puis 7 A, puis 13 A, et ainsi de
suite. La borne tire 1 495 W en moyenne sur 3 000.

Trois rampes : consigne atteinte au pas suivant, 30 s / 7 s, 120 s. Les deux
premières donnent la même trace, cycle pour cycle.

Le test FIGE le défaut. Le correctif devra inverser les deux assertions
marquées « DÉFAUT ». Aucune ligne du moteur n'est modifiée.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:46:18 +01:00
Patrick Schurig
9640ac73f2 debian : entrée de changelog 1.15.2+etm75
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-25 11:47:45 +01:00
Patrick Schurig
e449019390 Merge feature/hc-batterie dans release/etm75
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-25 11:47:34 +01:00
Patrick Schurig
b5ef257a36 docs : mentions de la démo neutralisées (commentaires et design seulement)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-25 11:42:34 +01:00
Patrick Schurig
21b71a691a decisions : référence régénérée (identifiants anonymisés, horloge fixe)
Elle remplace celle de e775f66, qui portait des ThingId bruts dans
shedding.open et des dates murales dans un test : le `check` n'y était pas
reproductible. Deux enregistrements successifs sont identiques (cmp).
Contre 76bf482 : les 85 fonctions communes sont identiques, sauf 12 lignes
dans 3 fonctions, où seuls le motif et le journal changent ; leurs actions
sont identiques.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:29:48 +01:00
Patrick Schurig
5c3cffbad3 Délestage visible : le test de la borne manuelle n'a plus d'horloge murale
Le passage en mode manuel ne restitue rien quand la borne est déjà dans
l'état de l'usager : il n'arme donc aucun verrou à l'heure réelle. Le
scénario peut reprendre une date fixe, et la référence `decisions` se
reproduit à l'octet près.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:28:13 +01:00
Patrick Schurig
facad90c2e Délestage visible : shedding.open publié en liste de variantes
Le JSON ne change pas. Mais une QStringList échappait au parcours par type
de l'enregistreur `decisions`, qui anonymise les ThingId des listes : la
référence portait les identifiants tirés au hasard d'une exécution, et le
`check` sortait en ÉCART à chaque passage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:28:13 +01:00
Patrick Schurig
e775f66e58 decisions : référence étendue au lot délestage visible
Hors épisodes de protection, IDENTIQUE à 76bf482 : les 85 fonctions
communes sont identiques octet pour octet, sauf trois. Celles-là gardent
leurs actions (things, proxy) identiques ; seuls le motif des bornes tenues
(OVERLOAD_THROTTLED, sans niveaux) et le bloc shedding changent :
testL4ShedsAPacAndSaysItForcedTheLock (1 ligne),
testBlockedDischargeShowsAtTheMeterAndL4Decides (2),
testOffPeakFullPowerYieldsToThePhaseLimit (9). Quatre fonctions nouvelles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:08:10 +01:00
Patrick Schurig
c85c822d05 docs : délestage visible — motif de la borne, journal court, notification (§8)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:05:53 +01:00
Patrick Schurig
1599acfba9 Délestage visible : tests de simulation
- borne abaissée, coupée, puis reprise par le cycle : une entrée au journal,
  et une fermeture qui notifie sans autre cause (contre-épreuve : un cycle
  identique ne notifie rien) ;
- borne en mode manuel, hors de loads[] : l'épisode s'ouvre et se ferme par
  notification, et la reprise amont le referme ;
- relais et SG-Ready délestés pendant 20 minutes : une entrée par charge,
  avant et après dans le langage de chaque mécanisme ;
- 21 épisodes : le plus ancien sort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:05:53 +01:00
Patrick Schurig
da6558fa8e Délestage visible : la protection publie ce qu'elle fait (OVERLOAD_THROTTLED, GetSheddingLog)
- Borne : executeChargingAction() devient virtuel (marque [ETM], corps amont
  inchangé). L'arbitre laisse partir la consigne, puis la lit. Une baisse ou
  une coupure de la protection de surcharge ouvre la tenue, et la borne
  publie OVERLOAD_THROTTLED {fromA, toA, cut, overshootW, limitA, o:phase}.
  La tenue se lève quand le cycle reprend la borne, ou quand la reprise
  amont lui rend son courant manuel. Les verrous de la borne sont lus : un
  geste refusé n'est pas publié comme un délestage.
- Cascade : shedWaterfallForOverload() note chaque délestage au journal,
  avant et après lus sur runtimeView(). Rien d'autre ne change.
- Télémétrie : bloc shedding {revision, open}, absent tant que rien n'a été
  délesté. Sa révision déclenche LoadTelemetryChanged à l'ouverture et à la
  fermeture d'un épisode.
- RPC : NymeaEnergy.GetSheddingLog, même droit que GetLoadTelemetry (table
  des droits et compte sans droit mis à jour).

Aucune consigne, aucun émetteur, aucun verrou ne change (invariant 2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:05:53 +01:00
Patrick Schurig
0aa357d5b6 Délestage visible : journal court des délestages (SheddingLog)
Un événement par ÉPISODE, en mémoire : il s'ouvre au premier geste de
protection et se ferme à la fin du premier cycle complet où la protection
ne tient plus la charge, daté du premier instant libre. « Avant » et
« après » portent la charge utile de mécanisme de l'adaptateur, sans liste
de types. Vingt épisodes ; le plus ancien épisode clos sort le premier.
Aucune décision ne le lit, et il est perdu au redémarrage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 11:05:53 +01:00
Patrick Schurig
76bf482b21 decisions : référence étendue au lot e2, suite (somme éco + confort dans le plafond)
Sans tarif à heures creuses, IDENTIQUE à b1c0ade : 81 fonctions, octet
pour octet, `run` compris. En heures creuses :
- testOffPeakFullPowerStaysUnderTheLimitOnABlindMeter : 7 → 13 lignes
  (variante au total aveugle ajoutée) ;
- testOffPeakFullPowerSwitchesToThreePhases : le recrédit de 1 380 W,
  refusé sous le plancher triphasé, est servi en confort dans le plafond ;
  achat 11 040 → 9 660 W, consigne totale inchangée (11 040 W) ;
- deux fonctions nouvelles : testOffPeakFullPowerAtDawnStaysInTheCeiling
  (13 lignes), testOffPeakFullPowerGivesNothingBackWhileSwitchingPhases (2).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:47:40 +01:00
Patrick Schurig
5c2a8df6d6 docs : heures creuses et batterie — décision D9, la somme éco + confort tient dans le plafond
§8 : D9 (2026-10-25, suite) — l'écart 4 est corrigé pour la pleine
puissance d'heures creuses ; la dérogation sans heure creuse et les phases
étiquetées restent à leur lot ; le compteur en retard est accepté.
§8.1 : l'écart 4 renvoie à §8.3 ; ce qui reste de la dérogation y est dit.
§8.3 : la règle, les mesures avant/après, les preuves — dont la garde de
bascule de §8.2, désormais éprouvée.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:41:52 +01:00
Patrick Schurig
e28e6375fd Heures creuses : la somme éco + confort de la borne tient dans son plafond (écart 4)
En heures creuses, le niveau éco d'une borne à pleine puissance vaut son
plafond de cycle, qui porte la limite de phase. À l'aube, la passe confort
lui ajoutait le surplus, et seul le courant maximal de la borne écrêtait
la somme : borne monophasée sur A, limite 16 A, plafond 13 A, 2 540 W de
surplus — 16 A servis, la phase A à 18 A. L'achat publié valait 2 990 W
quand le compteur n'importait que 450 W.

Pour une borne qui porte EV_OFF_PEAK_FULL_POWER :
- la passe confort sert le surplus DANS le plafond ;
- l'achat cède ce que le surplus finance (gridW, drawCommittedW,
  draw.committedW) : la somme reste le plafond ;
- la part confort n'a pas de plancher : il porte sur la somme, qui vaut
  le plafond. Sinon un surplus sous le plancher serait refusé, ou démarré
  par EV_GRID_START, un second achat en plus du premier.
Hors heures creuses et sans tarif, rien ne change.

Tests : testOffPeakFullPowerAtDawnStaysInTheCeiling (nuit, aube, plein
soleil : 13 A, achat 2 990 / 450 / 0 W, achat ≤ import du compteur) ;
testOffPeakFullPowerStaysUnderTheLimitOnABlindMeter étendu au total
aveugle (13, 15, 15… au lieu de 13, 16, 16…) ;
testOffPeakFullPowerGivesNothingBackWhileSwitchingPhases, la garde de
bascule enfin éprouvée (12 A, phase B à 24 A ; sans elle 15 A, B à 27 A).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 09:41:48 +01:00
Patrick Schurig
b1c0adec00 decisions : référence étendue au lot e2, suite (consigne posée près de la limite de phase)
Sans tarif à heures creuses : 79 fonctions, 24 056 lignes identiques
octet pour octet à d62365f. En heures creuses, deux fonctions changent
(testBlockedDischargeShowsAtTheMeterAndL4Decides, 2 cycles ;
testOffPeakFullPowerYieldsToThePhaseLimit, réécrit), plus les deux
nouvelles. check et ETM_DECISIONS_TRACES=1 check : IDENTIQUE
(24 168 lignes ; 22 254 lignes de trace).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:27:13 +01:00
Patrick Schurig
1302e9a04e docs : heures creuses et batterie — décision D8, la consigne se pose près de la limite de phase
- §8 : D8 (2026-10-25) — en heures creuses, le plafond de la borne lui
  rend son courant mesuré, moins 1 A par phase ; la tenue au plancher en
  surcharge (L4 décide) est confirmée.
- §8.1 : écarts ouverts 3 (double écrêtage hors heures creuses, lot à
  part) et 4 (partage achat/surplus à l'aube, avec la dérogation) ;
  l'oscillation renvoie à §8.2.
- §8.2 : la règle, la marge, la sécurité en dépassement, les hypothèses,
  les mesures avant/après et les preuves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:15:36 +01:00
Patrick Schurig
679e64b5b3 Heures creuses : le plafond de la borne lui rend ce qu'elle tire, moins 1 A (lot e2, suite)
Près de la limite de phase, la consigne d'une borne à pleine puissance
alternait à chaque cycle : le plafond était la place libre au compteur,
sans lui rendre sa propre consommation. Maison à 2 A par phase, borne
16 A : 16, 7, 16, 7 A sous 25 A ; 16, 14 sous 32 A ; 14, 6, 8, 6 sous 16 A.

En heures creuses, et pour la pleine puissance du lot e2 seulement, le
plafond est lu par EnergyArbitrator::evOffPeakAllowanceW() : limite moins
le tirage de la phase sans la borne, moins 1 A, à l'ampère inférieur. La
règle de l'échéance chez l'amont (adjustEvChargers()). Elle converge en
un cycle : 16 A tenus sous 25 et 32 A, 13 A sous 16 A.

- Courant rendu MESURÉ (currentPower), sur les phases où la borne le
  mesure ; rien rendu pendant une bascule ou sans mesure.
- Tirage de la maison sans la borne écrêté à zéro : une borne qui
  annonce plus que ce que le compteur voit reste à 1 A sous la limite.
- En dépassement, le plafond vaut au plus le courant mesuré moins 1 A :
  il baisse à chaque cycle, jusqu'au plancher, où L4 décide.
- Hors heures creuses et sans tarif : evPhaseAllowanceW(), inchangée.

Tests : testOffPeakFullPowerSettlesUnderThePhaseLimit (sept cas, douze
cycles) et testOffPeakFullPowerStaysUnderTheLimitOnABlindMeter, nouveaux ;
testOffPeakFullPowerYieldsToThePhaseLimit réécrit avec un compteur qui
voit la borne (mesure et non consigne, dépassement, plancher, L4) ;
testBlockedDischargeShowsAtTheMeterAndL4Decides : en dépassement, la
borne finit au plancher (L4 16 → 13 A, stratégie 9 A, L4 6 A) au lieu
d'être coupée.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 07:15:30 +01:00
Patrick Schurig
d62365fe3d decisions : référence étendue au lot e2 (pleine puissance en heures creuses)
72 fonctions sans borne en heures creuses : 23 812 lignes identiques octet
pour octet à la référence de b144c09. Changent, en heures creuses : les six
fonctions du lot e1 qui ont une borne (ECO_FLOOR_GRID 1 380 W →
EV_OFF_PEAK_FULL_POWER 3 680 W, et leur compteur d'entrée, qui inclut
désormais ce que la borne tire) ; s'ajoutent les trois fonctions du lot e2.
check et ETM_DECISIONS_TRACES=1 check : IDENTIQUE (24 068 lignes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:32:16 +02:00
Patrick Schurig
225fbc5a8d docs : heures creuses et batterie — décision D7, pleine puissance en heures creuses (lot e2)
§8 gagne D7 (2026-10-24) et §8.1 : la règle (minimum = plafond de cycle),
le choix des phases, la sécurité (LM-1500-h en heures creuses seulement, L4
garde le dernier mot), la fin de session au seuil mesuré, le motif
EV_OFF_PEAK_FULL_POWER. Deux points restent à trancher : le double
écrêtage hors heures creuses, et l'oscillation de la consigne près de la
limite de phase (le plafond ne rend pas à la borne ce qu'elle tire).
Lot futur, non implémenté : ne réserver que ce dont la maison a besoin
jusqu'au soleil, d'après la prévision. §2 renvoie à D7.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:22:15 +02:00
Patrick Schurig
1291f72d1f Heures creuses : la borne charge à la pleine puissance permise (lot e2)
Décision de Patrick du 2026-10-24. Dans les trois modes qui reportent en
heures creuses (Éco avec minimum, Éco min + échéance, Éco avec échéance) :

- le minimum servi au réseau en heures creuses est le PLAFOND de cycle de
  la borne (declared.maxPowerW), plus son plancher. Il porte le courant
  maximal, les phases et la limite de phase, et ne descend jamais sous le
  plancher : la stratégie rabaisse, L4 coupe. 3 680 W en monophasé 16 A,
  11 040 W en triphasé 16 A ;
- les phases se choisissent sur la pleine puissance, plus sur « courant +
  surplus » (EnergyArbitrator::enHeuresCreuses(), lu par EvAdapter). La nuit,
  une batterie qui couvre la maison et la voiture met le compteur à 0, et la
  règle de bascule gardait le monophasé : 3,7 kW au lieu de 11 ;
- la passe éco n'oppose plus à cette borne la marge d'UNE phase de la maison
  (LM-1500-h, déjà appliquée en tête de passe et en confort) : son plafond
  porte déjà l'autorisation, sur ses phases. Restreint aux heures creuses ;
- motif EV_OFF_PEAK_FULL_POWER {gridW, maxW, phases, until, endsAt,
  o:boundBy phaseLimit|floor}, rendu en français, à la place
  d'ECO_FLOOR_GRID sur la borne.

Hors heures creuses et sans heure creuse dans le tarif : rien ne change.
Le blocage de la décharge se lève toujours sur la puissance MESURÉE (230 W) :
une borne commandée à 16 A dont la voiture ne prend plus rien le lève.

Tests : testOffPeakChargesAtFullPower, testOffPeakFullPowerSwitchesToThreePhases,
testOffPeakFullPowerYieldsToThePhaseLimit. Les tests du lot e1 en heures
creuses attendent le plafond, et leur compteur inclut ce que la borne tire
désormais (TirageHcW) — sinon il fabriquait un surplus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 22:22:12 +02:00
Patrick Schurig
b144c09178 docs : heures creuses et batterie — les questions du lot e1 deviennent des décisions datées
Décisions de Patrick du 2026-10-24, §8 de DESIGN_HC_BATTERIE.md : toute la
décharge bloquée, maison comprise ; EV_GRID_START refusé hors heures
creuses (seul le mode boost charge tout de suite) ; blocage aussi par une
borne en mode Normal ; seuil de 230 W accepté ; étalement de l'échéance
renvoyé à un lot à part ; action « limite de décharge » et nouveau nom
d'enableDischarging décidés dans le dépôt du plugin. Aucune ne change le
code ; le garde reste compatible avec les deux actions actuelles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:25:19 +02:00
Patrick Schurig
1813fe80cf decisions : référence étendue à la fonction des écritures refusées (lot e1, suite)
Les 77 fonctions existantes donnent 23 950 lignes identiques octet pour
octet à la référence de 2cde57c (cmp, lignes de la nouvelle fonction
retirées). La référence s'étend des 96 lignes de
testRefusedWritesAreSpacedAndNeverDelayTheRelease ; check : IDENTIQUE
(24 046 lignes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:25:05 +02:00
Patrick Schurig
b007aae720 Batterie : une consigne refusée est réessayée en s'espaçant, la levée jamais (lot e1, suite)
Un onduleur qui refuse la consigne (GEN24 sans « Allow Control », exception
0x04) la voyait reposée à chaque cycle, avec un qCWarning à chaque fois,
toute la nuit, et `held` la publiait comme tenue.

- Une pose refusée (taux ou limite) ouvre une série de refus : la pose
  suivante attend 1, 2, 5, 10 puis 15 min au plus (marge de 5 s pour la
  gigue du cycle). La série se clôt à la première séquence acceptée
  jusqu'au bout (retour au tiers du minuteur) ou à la levée.
- La levée n'est jamais espacée : elle part au cycle même où la décision
  tombe, série en cours ou non.
- Journal : un seul qCWarning par série, les refus suivants en qCDebug, un
  qCInfo à l'acceptation qui clôt la série.
- Télémétrie : pendant une série, `held` est faux (et `dischargeBlocked`
  avec lui), et la batterie publie o:refusedWrites, o:refusedSince,
  o:nextAttemptAt. `held` faux plutôt qu'un drapeau à côté : un client qui
  ignore le champ nouveau ne doit pas afficher une protection absente.

Mock : propriété dynamique `refuserEcritures` sur la batterie pilotable.
Test testRefusedWritesAreSpacedAndNeverDelayTheRelease ; les six
contre-épreuves (espacement, avertissement, held, levée, deux fins de
série) le font rougir.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 11:19:15 +02:00
Patrick Schurig
2cde57ccf5 decisions : référence étendue aux six fonctions du lot e1
Sans elles, l'enregistrement est identique octet pour octet à la référence de
c5c3203 (23 799 lignes, cmp). Les 151 lignes ajoutées sont celles des six
scénarios d'heures creuses (testOffPeakDemoDay, …EndsAtSixAndOnDstDays,
testWithoutOffPeakTheEngineIsEtm74, testABatteryWithoutControlIsReadAsBefore,
testBlockedDischargeShowsAtTheMeterAndL4Decides,
testAMeterSilenceHandsTheBatteryBack). Deux enregistrements successifs
identiques.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 09:21:28 +02:00
Patrick Schurig
3cbcdeeca3 Test : le bloc storage se lit aussi par GetLoadTelemetry (lot e1)
La journée de la démo relit la télémétrie par la frontière RPC pendant le
blocage : le bloc passe la validation du schéma déclaré (o:storage), et le Get
rend ce qui a été publié.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 08:56:24 +02:00
Patrick Schurig
4ffa8391cf docs : heures creuses et batterie — design, sûreté L2, inventaire du mock (lot e1)
DESIGN_HC_BATTERIE.md : la règle des heures creuses, les modes de borne
concernés et le changement de comportement, la lecture du modèle SunSpec 124
(StorCtl_Mod = bit qui ACTIVE la limite, OutWRte en %, séquence NO_DISCHARGE
du plugin) qui fixe l'ordre des écritures, le minuteur de retour comme sûreté
(et l'hypothèse non levée du plugin sur le 124), l'invariant 8 et les règles de
réserve, ce qui est publié, les preuves, et ce que le lot ne fait pas.

SAFETY.md : le repli L2 lève la limite de décharge explicitement.
INVENTAIRE_MOCK.md : la classe controllableStorage, et ce qu'elle n'émule pas.
DESIGN_PREVISION_PV.md §32.4 : la décision e1 est prise, pointeur.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 08:38:29 +02:00
Patrick Schurig
d9372c14ba Heures creuses : la borne attend les HC, la batterie ne se décharge pas dans la voiture (lot e1)
Décisions de Patrick du 2026-10-24. Sans heure creuse dans le tarif saisi
(aucun tarif, tarif unique, prix tous égaux), rien de ceci ne s'exécute.

LA BORNE — modes « Éco avec minimum », « Éco min + échéance », « Éco avec
échéance » (EvAdapter déclare needs.offPeakShift ; le scheduler décide) :
- hors heures creuses : surplus seulement. Le minimum réseau est retiré,
  EV_GRID_START aussi (le minimum par une autre porte). Motif
  EV_WAITING_OFF_PEAK {at "22:00", startsAt, budgetW}, rendu « … attend les
  heures creuses (22:00) ». Le plafond de soutirage et le refus gardent la tête.
- en heures creuses : le plancher de la borne est servi au réseau (passe éco,
  bornée par l'autorisation de soutirage). L'échéance reste au proxy, inchangée.
- Eco et Normal ne sont pas touchés.

LA BATTERIE — StorageDischargeGuard exécute, le rule-based décide (y compris en
ombre d'un optimiseur) :
- heure creuse ET bornes qui TIRENT ≥ 230 W (mesuré) ET batterie pilotable :
  dischargingRate = 0 PUIS enableDischarging = true (StorCtl_Mod bit DiSCHARGE
  = limite active, OutWRte = 0 : la séquence NO_DISCHARGE du plugin) ;
  rafraîchi au tiers du minuteur de retour (revertTime) ;
- sinon : levée EXPLICITE, enableDischarging = false puis dischargingRate au
  maximum. L2 lève aussi. Une limite à 0 % trouvée active au démarrage est
  reprise (rafraîchie ou levée).
- jamais gridCharging, enableCharging, chargingRate, ni un taux négatif.
- générique : interface energystorage et états présents. Sans les états
  (noDischargeControl), sans minuteur (noRevertTimer) ou minuteur < 120 s
  (revertTimerTooShort) : non commandée, STORAGE_NOT_PROTECTED.
- publié : bloc `storage` de la télémétrie (dischargeBlocked, decision,
  batteries[] avec ce qui est LU), absent sans heure creuse ou sans batterie ;
  ligne « [Arbitre] Batterie → … » au journal.

Le blocage ne verse rien au budget : surplus = compteur + PV (invariant 8),
le soutirage ajouté se mesure au cycle suivant, L4 garde le dernier mot.

Mock : classe controllableStorage (états modifiables du sunspecStorage,
paramètre revertTime). Tests : journée de la démo, 06:00 sur jours de 24/23/25 h,
etm74 sans heure creuse, batteries non pilotables ou absente, soutirage et L4,
repli L2 — les écritures lues à la frontière nymea (actionExecuted), l'onduleur
modélisé hors du moteur. Le commentaire du test §11 est réécrit comme il le
demandait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 08:34:21 +02:00
Patrick Schurig
920cf854da Tarif : les heures creuses sont la ou les zones au prix le plus bas (lot e1)
TariffConfig::zonesCreuses() : les zones au prix minimal d'un tarif à zones
horaires dont les prix diffèrent. Aucun tarif, tarif unique ou prix tous
égaux : aucune heure creuse. Le libellé ne décide pas — « HC » n'a pas de
code, le prix se compare.

enHeuresCreuses() et prochaineBascule() lisent l'heure murale minute
absolue par minute, comme importSeries() : bascules exactes les jours de
23 h (nuit de 7 h) et de 25 h (nuit de 9 h, 02:30 deux fois creuse),
épinglées sur des époques calculées hors de Qt (zoneinfo).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 08:26:01 +02:00
Patrick Schurig
c5c32035d1 debian: 1.15.2+etm74 — protocole optimiseur 1.3 (forme d'error.data, token avant la majeure)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-23 15:02:23 +02:00
Patrick Schurig
9b974ff974 Fusion de feature/protocole-1.3 dans release/etm74 (protocole optimiseur 1.3 : forme d'error.data, token avant la majeure)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
2026-10-23 15:02:23 +02:00
Patrick Schurig
651181f84e Conformité : forme d'error.data et ordre token puis majeure (protocole 1.3)
Deux fonctions, par la connexion de sonde :
- testAnInvalidContextIsRefusedWithItsProblems : un GetPlan de contexte invalide
  ({"timestamp": "valeur-sonde:7Qz9"}) → -32602 à son id, connexion gardée ; la session de sonde
  jugée par le validateur publié contre le contrat EMBARQUÉ (--responses-only) : forme exigée
  d'un optimiseur qui s'annonce 1.3 ou plus, AVERTISSEMENT seulement pour un 1.2 ; le contrôle
  doit avoir eu lieu (GetPlan.error.data compté) ; garde indépendante : la valeur reçue n'est
  pas dans error.data, quelle que soit la version.
- testTheTokenIsCheckedBeforeTheMajor : token faux et autre majeure → -32001 puis fermeture ;
  -32602 d'un optimiseur 1.2 averti ; SKIP sans token attendu.

Éprouvé contre l'optimiseur de référence : 1.3 conforme (11 verts + 1 SKIP sans token, 3/3 avec
token) ; old-data annoncé 1.3 refusé, annoncé 1.2 averti ; echo-value refusé en 1.3 comme en 1.2 ;
major-first refusé en 1.3, averti en 1.2 ; référence annoncée 1.2 verte sans avertissement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:15:10 +02:00
Patrick Schurig
d961b73ed7 Validateur du contrat : juge error.data selon la version de l'optimiseur (protocole 1.3)
tools/etm-powersync-schema-check :
- lit methods[].errors[].x-etm-data-schema : error.data d'une réponse error de ce code est jugé
  contre ce schéma (audité comme les autres) ; exigé à partir de x-etm-data-required-from, la
  version étant celle que l'optimiseur annonce dans SON Handshake ; en deçà, écart écrit en
  AVERTISSEMENT, sans effet sur le code de sortie ; version inconnue = celle du contrat ;
- truncated n'accompagne que 50 problèmes exactement ;
- aucune valeur reçue recopiée dans error.data : écart dès 1.2 (règle de 1.2). Le contrôle ne
  porte que sur les valeurs qu'un chemin conforme ne peut pas contenir — il ne peut pas accuser
  un chemin conforme (domain « battery » / params.battery) ;
- --error (un objet error, avec --request et --handshake) et --responses-only (session de sonde,
  requêtes fautives à dessein) ; RÉSUMÉ compte les avertissements.

Tests : forme exigée en 1.3 et avertie en 1.2 sur de vraies sessions de l'optimiseur de
référence (normal, old-data, echo-value × 1.3, 1.2), forme ligne à ligne, recopie sans faux
positif, contre-épreuve « sans x-etm-data-schema, rien n'est jugé ». Les cinq nouveaux tests
échouent avec le validateur précédent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:13:57 +02:00
Patrick Schurig
8c7c33be39 Optimiseur de référence : aligné sur le protocole 1.3
- Annonce 1.3. error.data du -32602 d'un contexte invalide dans la forme normée :
  {"problems":[{"path":"params.timestamp","problem":"missing"|"type"|"format"}]}, 50 problèmes
  au plus et truncated au-delà — plus de chemin « /timestamp » ni de phrase libre.
- Handshake : le token était déjà jugé avant la majeure ; l'ordre est désormais écrit et nommé.
- Pour les contre-épreuves de la conformité : --announce VERSION (se présenter en 1.2) et trois
  modes, old-data (forme d'avant 1.3, {"problemes": [phrases]}), echo-value (recopie de la valeur
  reçue), major-first (la majeure jugée avant le token).

Suite refoptimizer 8/8 ; test du validateur vert avec l'outil inchangé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 11:13:17 +02:00