Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HE6X3TcKdiuCucBHA2XX4n
33 KiB
Heures creuses pour la borne, batterie protégée pendant sa charge — lot e1 (2026-10-24)
Décisions de Patrick du 2026-10-24. Ce document décrit ce que le code fait, les preuves qui le
rejouent, et ce qu'il ne fait pas. Code : etm/tariff/tariffconfig.*,
etm/scheduler/rulebasedscheduler.cpp, etm/adapters/storagedischargeguard.*,
etm/energyarbitrator.cpp (buildContext, update, applyDegradedMode).
1. Les heures creuses
Règle : les heures creuses sont la ou les zones au prix le plus bas d'un tarif à zones
horaires (tou) dont les prix diffèrent (TariffConfig::zonesCreuses()).
- « HC » n'a pas de code : le libellé est une donnée de l'installateur. Le prix se compare.
- Plusieurs zones au même prix minimal sont toutes creuses (une nuit et un début d'après-midi saisis comme deux zones).
- Aucune heure creuse sans tarif, avec un tarif unique (
fixed), ou avec des zones toutes au même prix. Dans ces trois cas rien de ce lot ne s'exécute : le moteur est celui d'etm74 (preuvedecisionsIDENTIQUE, ettestWithoutOffPeakTheEngineIsEtm74). - Lecture murale, minute absolue par minute (
enHeuresCreuses(),prochaineBascule()), dans le fuseau de la box relu à chaque cycle. Les jours de 23 h et de 25 h sont exacts (nuit 22:00-06:00 de 7 h et de 9 h), épinglés sur des époques calculées hors de Qt. - Le prix n'est toujours lu par aucune décision, et
SurplusContext::tariffn'est toujours pas rempli : l'optimiseur ne reçoit rien de neuf. Les heures creuses vivent dansSurplusContext::offPeak, interne au moteur.
2. La borne : surplus le jour, complément en heures creuses
Modes concernés — ChargingModeEcoWithMinCurrent, ChargingModeEcoMinWithTargetTime,
ChargingModeEcoWithTargetTime : ceux dont le besoin réseau peut attendre. EvAdapter le
DÉCLARE (LoadNeeds::offPeakShift, offPeakMinPowerW = le plafond de cycle de la borne depuis
le lot e2, son plancher au lot e1 — §8, D7), le scheduler le DÉCIDE (règle 2). ChargingModeEco ne demande jamais le réseau, ChargingModeNormal est
piloté par l'utilisateur : tous deux sont inchangés.
| hors heures creuses | en heures creuses | |
|---|---|---|
minimum réseau (minPowerWAlways) |
retiré — surplus seulement | le plafond de la borne (lot e2, §8 D7) |
démarrage sous tolérance (EV_GRID_START) |
refusé (le minimum par une autre porte) | inchangé |
| motif d'une consigne nulle | EV_WAITING_OFF_PEAK {at, startsAt, budgetW} |
motifs ordinaires |
| motif de la consigne servie | — | EV_OFF_PEAK_FULL_POWER (lot e2, §8 D7) |
échéance (EV_DEADLINE, proxy) |
inchangée | inchangée |
Le changement de comportement : une borne « Éco avec minimum » ne prend plus ses 6 A au
réseau le soir. Elle charge au surplus tant qu'il y en a, puis attend 22:00. Le motif le dit :
« Borne attend les heures creuses (22:00) — hors heures creuses, surplus seulement (… W) ».
EV_WAITING_OFF_PEAK remplace SURPLUS_INSUFFICIENT, BELOW_MIN_POWER et BATTERY_RESERVE,
parce que la cause de la consigne nulle n'est plus le manque de surplus. Le plafond de soutirage
(DRAW_CAP), le refus (RECREDIT_SUSPENDED), la limite de phase, l'indisponibilité et les
verrous gardent la tête : ils diraient la même chose en heures creuses.
En heures creuses, le plancher est servi au réseau par la passe éco (ECO_FLOOR_GRID),
bornée par l'autorisation de soutirage ; le confort ajoute le surplus s'il y en a. Lot e2 :
c'est désormais le plafond, sous EV_OFF_PEAK_FULL_POWER — §8, D7. La borne
déclare un plafond écrêté par evPhaseAllowanceW() — en heures creuses par
evOffPeakAllowanceW(), qui lui rend son courant mesuré moins 1 A (§8.2, D8) —, et
verifyOverloadProtection() garde le dernier mot : l'invariant 2 tient sans rien ajouter.
L'échéance : la puissance nécessaire pour tenir une échéance reste celle du proxy amont, qui force la charge à pleine puissance au point de non-retour. Ce point est calculé sur l'échéance, pas sur la fin des heures creuses : une échéance trop proche peut donc encore se servir en heures pleines. Une échéance est une promesse de l'utilisateur, le tarif ne la rompt pas. Étaler la charge sur les heures creuses demanderait une énergie restante mesurée (LM-1009) : ce sera un lot à part (§8, D3).
Le refus d'EV_GRID_START hors heures creuses est confirmé (§8, D2) : seul le mode boost
(ChargingModeNormal) charge tout de suite.
À 06:00, le minimum est retiré : une borne qui charge encore s'arrête (au premier cycle où son verrou d'enclenchement le permet, 300 s par défaut).
3. La batterie : décharge bloquée pendant la charge de la voiture en heures creuses
3.1 Ce que le modèle SunSpec 124 dit — et ce que le plugin écrit
Lu dans le plugin sunspec (integrationpluginsunspec.cpp, sunspecstoragelogic.*,
froniusrules.*) et dans la définition du modèle 124 de libnymea-sunspec :
StorCtl_Modest un champ de bits — « Activate hold/discharge/charge storage control mode » : bit 0CHARGE, bit 1DiSCHARGE. Le bit active la limite correspondante, il n'« autorise » pas l'opération.OutWRteest la limite de décharge, en pourcentage de la puissance maximale (% WDisChaMaxdans le modèle, « % deWChaMax» dans la documentation du plugin). 0 % vaut 0 W quelle que soit la référence : l'unité n'a pas d'incidence sur la valeur écrite ici, ni le facteur d'échelleInOutWRte_SF(que le plugin relit avant chaque écriture, AC-12).- L'état
enableDischargingdu plugin est ce bit (readStates()), et l'action le pose par lecture-modification-écriture deStorCtl_Mod. L'étatdischargingRateestOutWRte. - Le contrat du plugin (AC-11,
FroniusRules::planBatteryMode()) le confirme :NO_DISCHARGE=StorCtl_Mod = 2,OutWRte = 0;NORMAL=StorCtl_Mod = 0,OutWRte = 100.
Donc enableDischarging = false LÈVE la limite — il ne bloque rien. La séquence qui bloque
réellement la décharge est dischargingRate = 0, puis enableDischarging = true. Le retour
à l'automatisme est enableDischarging = false, puis dischargingRate = 100 (le maximum de
l'état).
Le plugin permet de l'écrire, en deux actions. Chacune est un plan d'écriture complet (ordre imposé, relecture, minuteur de retour réarmé en dernier). Elles ne sont pas atomiques : le garde les enchaîne (la seconde part à la confirmation de la première), dans l'ordre qui rend chaque état intermédiaire sûr :
- pose : le taux d'abord. Tant que la limite est inactive, le taux est inerte ; quand elle s'active, il vaut déjà 0. Si la première écriture échoue, la seconde ne part pas — la limite reste inactive, la batterie en automatique ;
- levée : la limite d'abord, l'automatisme revient tout de suite ; le taux est remis au maximum pour qu'une limite réactivée plus tard ne reparte pas à 0 %.
FroniusRules::planBatteryMode(NO_DISCHARGE) écrirait les deux registres en une séquence, mais
aucune action du plugin ne l'emprunte. L'exposer est décidé, dans le dépôt du plugin (§8, D6) ;
le garde reste compatible avec les deux actions actuelles.
3.2 Quand
Heure creuse ET bornes qui tirent réellement ≥ 230 W (mesuré, toutes bornes, mode manuel
compris) ET au moins une batterie pilotable. Sinon la batterie est rendue à son automatisme,
et une consigne tenue est levée explicitement. La décision est prise par le rule-based à
chaque cycle (RuleBasedScheduler::storageDecision()), y compris en ombre d'un optimiseur.
La batterie est bloquée au cycle où la voiture tire : au cycle de 22:00 la borne vient d'être commandée et lit encore 0 W, le blocage part à 22:01. Elle est rendue au cycle où la borne ne tire plus (fin de charge) ou à 06:00.
Ce que le blocage bloque : toute décharge, maison comprise. « Ne pas se décharger dans la
voiture » ne s'exprime pas autrement sans asservir OutWRte à la consommation de la maison —
une boucle de régulation que le lot ne fait pas. C'est le geste voulu (§8, D1), comme le
blocage par une borne en mode boost (D4) et le seuil de 230 W (D5).
3.3 Pilotable, ou non
Par l'interface energystorage et les états présents — aucune ThingClass n'est nommée
(StorageDischargeGuard::capacite(), question posée à la classe) :
| cause | condition | effet |
|---|---|---|
| — | enableDischarging (booléen) et dischargingRate (0 admis) modifiables, revertTime ≥ 120 s |
commandée |
noDischargeControl |
un des deux états absent ou non modifiable | lue comme avant, STORAGE_NOT_PROTECTED |
noRevertTimer |
pas de paramètre revertTime, ou 0 |
idem — un moteur arrêté la laisserait bloquée sans fin |
revertTimerTooShort |
revertTime < 120 s |
idem — elle expirerait entre deux cycles |
3.4 Le minuteur et le rafraîchissement
Chaque écriture du plugin se termine par InOutWRte_RvrtTms = revertTime (300 s sous Fronius,
FroniusRules::DefaultRevertTimeSeconds). Le garde réécrit la séquence dès que la dernière a
le tiers du minuteur (100 s) : à un cycle par minute, deux écritures sont espacées de 120 s,
180 s si un cycle manque — toujours sous 300 s. Il la réécrit aussi, sans attendre, si la
batterie ne rapporte plus la limite (onduleur revenu seul à l'automatisme).
La sûreté est le minuteur, pas le moteur : si nymead s'arrête, plus rien ne réécrit, et le
GEN24 reprend son automatisme au plus 300 s après la dernière écriture. Sous compteur muet (L2),
le garde lève la limite explicitement. Au redémarrage, une limite à 0 % trouvée active est
reprise (rafraîchie ou levée).
⚠️ Hypothèse non levée, et elle est du plugin : la sémantique du minuteur est documentée par le constructeur au chapitre général et illustrée sur le modèle 123, pas sur le 124 (AC-10 du plugin, « décision D4 non validée »). Les tests la modélisent ; seule une mesure sur un GEN24 réel la prouve.
3.4 bis Un onduleur qui refuse (suite du lot e1)
Un GEN24 dont « Allow Control » est désactivé répond 0x04 à toute écriture. Avant cette suite, la
batterie ne rapportant jamais la limite, le garde reposait la séquence à chaque cycle, avec un
qCWarning à chaque fois, toute la nuit.
- Espacement : 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, à compter de la pose refusée, avec une marge de 5 s pour la gigue d'un cycle par minute. Quinze minutes au plus : un réglage corrigé en cours de nuit est repris dans le quart d'heure.
- Fin de série : à la première séquence acceptée jusqu'au bout (taux puis limite), le rythme normal reprend (tiers du minuteur) ; ou à la levée, qui clôt la série — la pose suivante est une consigne neuve.
- La levée n'est jamais espacée : rendre l'automatisme part au cycle même où la décision
tombe, série en cours ou non. Elle n'est pas répétée si elle est refusée (le minuteur de
l'onduleur la fera) ; son refus reste un
qCWarning. - Journal : un seul
qCWarningpar série (« … REFUSÉE (…) : la décharge n'est PAS bloquée … »), les refus suivants et les nouvelles tentatives enqCDebug, et unqCInfoà l'acceptation qui clôt la série (« … ACCEPTÉE après N refus »). - Télémétrie : pendant une série,
heldest faux — etdischargeBlockedavec lui — et la batterie publierefusedWrites(poses refusées d'affilée),refusedSinceetnextAttemptAt. La décision resteSTORAGE_DISCHARGE_BLOCKED: c'est ce que le moteur a décidé, l'exécution est ce qui échoue.
Pourquoi held faux plutôt que held vrai plus un drapeau : held et dischargeBlocked
sont lus comme « la batterie ne se décharge pas ». Un client qui ignore un champ nouveau —
c'est-à-dire tous les clients d'aujourd'hui — continuerait d'afficher une protection qui
n'existe pas. Avec held faux, le client existant dit vrai sans changement, et le client à jour
lit en plus pourquoi et jusqu'à quand. Le passage de dischargeBlocked à faux notifie, ce qu'un
drapeau à côté n'aurait pas fait. Un refus se lit au cycle suivant la pose : le verdict de
l'onduleur arrive après la publication du cycle qui a écrit, comme observed.
3.5 Sécurité et invariant 8 (« pas de boucle »)
La décision ne lit ni le budget ni la réserve, et n'y verse rien. Le budget reste compteur +
PV mesurés. Les règles de réserve (rulebasedscheduler.cpp, « Prise en compte du stockage »)
restent justes sous blocage :
- la nuit, batterie bloquée et sans PV :
powerW≈ 0, aucune branche ne modifie le budget, qui vaut −(import). Le blocage augmente l'import, le budget le constate : aucun surplus n'est fabriqué (testBlockedDischargeShowsAtTheMeterAndL4Decides,surplusW == −import) ; - sous la réserve, le surplus positif est annulé : il n'y en a pas la nuit, la règle est sans effet ;
- au-dessus de la réserve et en charge (PV avant 06:00 l'été) : la charge de la batterie est rendue au budget. C'est vrai sous blocage aussi : bloquer la décharge ne touche pas la charge, et le GEN24 réduit sa charge pour couvrir une consommation nouvelle — ce qu'il ne peut plus couvrir vient alors du réseau, pas de la batterie, et le compteur le montre au cycle suivant ;
- en décharge (
powerW< 0, si l'onduleur ignorait la consigne) : retranchée, comme avant — le sens prudent.
Le soutirage ajouté — ce que la batterie couvrait passe au réseau — se mesure au cycle
suivant : la marge de soutirage du contexte (houseDrawMargin()) et verifyOverloadProtection()
le lisent au compteur, et coupent la borne s'il dépasse (éprouvé avec une limite de 25 A). La
borne coupée ne tire plus : la batterie est rendue au cycle d'après.
Oscillation possible, signalée : sur une installation au bord de sa limite de phase, la suite « bloquer → la phase dépasse → L4 coupe la borne → rendre → la batterie couvre → la borne redémarre » peut se répéter. Sa période est bornée par le verrou d'enclenchement de la borne (300 s), et L4 tient la sécurité à chaque tour.
Hystérésis de la réserve de 20 % : non touchée. Effet indirect seulement : la batterie ne se vide plus dans la voiture la nuit, elle passe plus souvent la réserve le matin.
4. Ce qui est publié
- Journal :
[Arbitre] Batterie → Décharge de la batterie bloquée : la voiture charge en heures creuses (1380 W, jusqu'à 06:00), à chaque cycle où la question se pose ;[Batterie] … décharge BLOQUÉEet… RENDUE à l'automatismeaux transitions (qCInfo), le rafraîchissement enqCDebug, un refus d'écriture enqCWarning(règle 7-d) — une fois par série de refus pour la pose (§3.4 bis). - Télémétrie (
GetLoadTelemetryetLoadTelemetryChanged, même charge utile) : un blocstorage, absent sans heure creuse ou sans batterie —dischargeBlocked(ce que le moteur TIENT après le dispatch),decision{code, params}, etbatteries[]avec ce qui est LU (observed.enableDischarging,observed.dischargingRate) ; pendant une série de refus,heldfaux etrefusedWrites,refusedSince,nextAttemptAt. Une transition du bloc notifie. - Motifs :
EV_WAITING_OFF_PEAK,STORAGE_DISCHARGE_BLOCKED,STORAGE_AUTOMATIC(peak|evNotCharging|degraded),STORAGE_NOT_PROTECTED. Aucun n'est émissible par un optimiseur. - Aucun flux sortant nouveau (invariant 10) : le garde parle au ThingManager, comme tous les adaptateurs.
5. Jamais
gridCharging, enableCharging, chargingRate ne sont jamais écrits, ni un dischargingRate
négatif (charge forcée). La recharge de la batterie sur le réseau est l'offre Pro. Le test de la
journée le vérifie sur toutes les écritures exécutées, et la contre-épreuve qui écrit
gridCharging le fait rougir.
6. Preuves
| Preuve | Ce qu'elle rejoue |
|---|---|
testOffPeakIsTheCheapestZone (loadmodel) |
la règle ; aucun / unique / prix égaux ; jours de 23 et 25 h |
testOffPeakDemoDay |
12 h, attente à 18 h, refus d'EV_GRID_START, reprise à 22:00, blocage à 22:01, minuteur tenu 88 cycles, levée à la fin de charge, aucune écriture interdite |
testOffPeakEndsAtSixAndOnDstDays |
arrêt et levée à 06:00, jours de 23, 24 et 25 h |
testWithoutOffPeakTheEngineIsEtm74 |
trois formes sans heure creuse : minimum à 18 h, ni bloc ni écriture, trames identiques |
testABatteryWithoutControlIsReadAsBefore |
sans états, sans minuteur, absente |
testBlockedDischargeShowsAtTheMeterAndL4Decides |
invariant 8, soutirage mesuré, L4 coupe, levée |
testAMeterSilenceHandsTheBatteryBack |
L2 lève explicitement |
testRefusedWritesAreSpacedAndNeverDelayTheRelease |
onduleur qui refuse : poses à 0, 1, 3, 8, 18, 33, 48 min, un avertissement par série, held faux ; acceptation → rythme normal ; levée immédiate en pleine série, qui clôt la série |
decisions |
IDENTIQUE à la référence sans tarif |
Les écritures sont lues à la frontière nymea (ThingManager::actionExecuted), et le GEN24 est
modélisé dans le test (limiteTenue()), jamais dans le moteur ni dans le mock.
7. Ce que ce lot ne fait pas
- La recharge de la batterie sur le réseau ; toute optimisation par le prix (offre Pro).
- L'étalement d'une échéance sur les heures creuses (§2) — lot à part (§8, D3).
- Une action atomique « limite de décharge » dans le plugin (
planBatteryMode(NO_DISCHARGE)existe et n'est pas exposée), et un état au nom moins trompeur queenableDischarging— décidés, faits dans le dépôt du plugin (§8, D6). - La batterie comme charge rangée du waterfall (lot 3f,
spec_loadmodel.md§14).
8. Décisions de Patrick (2026-10-24)
Les questions ouvertes du lot e1 sont tranchées. Chacune confirme le comportement décrit plus haut : aucune ne change le code.
| # | Question | Décision du 2026-10-24 |
|---|---|---|
| D1 | Bloquer toute la décharge, maison comprise, pendant la charge de la voiture en heures creuses (§3.2) ? | Oui. C'est le geste voulu ; aucune régulation de OutWRte sur la consommation de la maison. |
| D2 | Refuser EV_GRID_START hors heures creuses pour les trois modes concernés (§2) ? |
Confirmé. En mode Éco, la borne attend les heures creuses ; seul le mode boost (ChargingModeNormal, piloté par l'utilisateur) charge tout de suite. |
| D3 | Étaler une échéance sur les heures creuses (§2) ? | Plus tard, lot à part. Il demande une énergie restante mesurée (LM-1009). |
| D4 | Une borne en ChargingModeNormal qui charge en heures creuses déclenche-t-elle le blocage (§3.2) ? |
Oui, c'est le but : la mesure porte sur toutes les bornes, mode compris. |
| D5 | Seuil de « la voiture charge réellement » : 230 W sur la somme des bornes (§3.2) ? | Accepté. |
| D6 | Plugin SunSpec : une action « limite de décharge » en une séquence (planBatteryMode(NO_DISCHARGE)), et enableDischarging renommé ou redécrit (§3.1, §7) ? |
Oui aux deux, dans le dépôt du plugin, à la charge de l'orchestrateur. Le garde ne change pas : il reste compatible avec les deux actions actuelles (dischargingRate puis enableDischarging). |
| D7 | À quelle puissance la borne charge-t-elle en heures creuses (lot e2) ? | À la pleine puissance permise, et non plus au plancher. La session finit plus tôt, et la décharge de la batterie n'est bloquée que pendant ce temps-là : ensuite, elle couvre la maison pour le reste de la nuit. Détail en §8.1. |
| D8 | (2026-10-25) Près de la limite de phase, la consigne de la borne alterne à chaque cycle (16, 7, 16, 7 A sous 25 A). Corriger avant la démo ? | Oui, maintenant. En heures creuses, le plafond de la borne lui rend ce qu'elle tire, mesuré, moins 1 A par phase. Détail en §8.2. Le même jour : la borne tenue au plancher en surcharge (L4 décide de couper) est confirmée ; les écarts 3 et 4 de §8.1 sont reportés. |
| D9 | (2026-10-25, suite) L'écart 4 n'est pas qu'un défaut de publication : à l'aube, le surplus s'ajoute au plafond, qui porte la limite de phase. Borner la somme avant la démo ? | Oui, avant la démo. La somme éco + confort ne dépasse jamais declared.maxPowerW, et l'achat publié ne dépasse pas ce qui est réellement acheté. Détail en §8.3. La dérogation sans heure creuse, les phases étiquetées : plus tard, chacune dans son lot. Le compteur en retard sur la borne est accepté tel quel. |
La mesure du minuteur de retour sur un vrai GEN24 (hypothèse AC-10 du plugin, §3.4) reste ouverte : elle relève du plugin et d'un essai sur matériel réel.
8.1 D7 — pleine puissance en heures creuses (lot e2)
Pourquoi. Au plancher (6 A, 1,4 kW en monophasé), une nuit de 22:00 à 06:00 livre au plus 11 kWh : une session de 10 à 25 kWh n'est pas finie au départ. Et la décharge de la batterie reste bloquée toute la nuit, puisque la voiture charge toute la nuit : la batterie est encore pleine le matin, et le surplus solaire part au réseau.
La règle. Dans les trois modes qui reportent en heures creuses (§2), le minimum servi au
réseau en heures creuses est le plafond de cycle de la borne (declared.maxPowerW) :
- il porte le courant maximal de la borne, ses phases, et la limite de phase
(
evPhaseAllowanceW(); depuis D8,evOffPeakAllowanceW(), §8.2) — rien n'est retiré de ces écrêtages ; - il ne descend jamais sous le plancher : la limite de phase rabaisse la borne, elle ne la
coupe pas ; couper reste le geste de
verifyOverloadProtection()(L4), qui garde le dernier mot ; - hors heures creuses, et sans heure creuse dans le tarif, rien ne change.
Par exemple : 3 680 W en monophasé 16 A, 11 040 W en triphasé 16 A.
Les phases. En heures creuses, une borne qui peut charger en triphasé charge en triphasé. La règle de bascule du manager est inchangée ; c'est le courant qu'on lui présente qui change. Hors heures creuses, c'est « courant de la borne + surplus ». En heures creuses, c'est la pleine puissance de la borne. Raison : la nuit, « courant + surplus » bride dans un cas précis — une batterie (non pilotable, ou pas encore bloquée) couvre la maison et la voiture, le compteur lit ≈ 0, et la référence vaut le courant que la borne tire déjà. À 6 A en monophasé, la règle garde le monophasé : 3,7 kW au lieu de 11. La limite de phase n'entre pas dans le choix, et c'est sans perte : par phase, elle borne pareil une monophasée et une triphasée, donc trois phases donnent toujours au moins autant.
Sécurité. Le plafond d'une borne porte déjà l'autorisation de soutirage, résolue sur les
phases qu'elle occupe et multipliée par leur nombre. La passe éco ne lui oppose donc pas en plus
la marge d'une phase de la maison (LM-1500-h, déjà appliquée en tête de passe et en passe
confort). Sans cela, une triphasée serait servie à 7 A au lieu de 16. Une autre charge allumée
la nuit réduit le plafond au cycle suivant ; sous le plancher, la borne est tenue au plancher, et
L4 coupe au dépassement. Cette exemption ne vaut qu'en heures creuses : sans heure creuse, le
plancher éco d'une borne reste écrêté par la marge de la maison, comme avant.
Surcharge au plancher — confirmé le 2026-10-25. En heures creuses, quand la limite de phase
ne laisse plus le plancher, la stratégie tient la borne à son plancher (boundBy: floor) et L4
décide de la couper, comme pour la dérogation. On n'est pas revenu au « rien acheté » du lot e1.
Écarts ouverts (rien n'est changé ; décision du 2026-10-25 : plus tard, chacun dans son lot) :
- Écart 3 — le double écrêtage hors heures creuses. Sans heure creuse, le plancher éco d'une
borne triphasée est encore comparé à la marge d'une phase de la maison (le double
écrêtage que
LM-1500-hrefuse ailleurs). L'étendre changerait des décisions sans tarif : lot à part. - Écart 4 — le partage achat/surplus en heures creuses. Corrigé pour la pleine puissance
d'heures creuses (D9, §8.3). À l'aube, le surplus s'ajoutait au niveau éco : la consigne
(
estimatedPowerW) pouvait dépasser le plafond de cycle, et donc la limite de phase qu'il porte. La dérogation, elle, ne cumule pas (sa passe confort est sautée) ; son achat publié reste entier quand le soleil la finance en partie — c'est la conséquence comptable déjà écrite dans le code, et elle reste à reprendre avec la dérogation. - L'oscillation près de la limite de phase était le troisième point de cette liste. Elle est corrigée : §8.2.
Fin de session. Le blocage de la décharge se lève sur la puissance mesurée des bornes, au seuil de 230 W (D5). Une borne commandée à 16 A dont la voiture ne prend plus rien (pleine, ou session arrêtée par le véhicule) ne tient donc pas la batterie bloquée.
Motif. EV_OFF_PEAK_FULL_POWER {gridW, maxW, phases, until, endsAt, o:boundBy} remplace,
en heures creuses, ECO_FLOOR_GRID sur la borne. gridW est la puissance retenue, maxW la
pleine puissance du matériel aux phases visées. boundBy n'est présent que si la protection de
surcharge l'a rabaissée : phaseLimit (à ce que la limite laisse) ou floor (tenue au
plancher, L4 décide de la suite). Rendu : « Borne charge à pleine puissance en heures creuses :
11040 W achetés au réseau (3 phases, jusqu'à 06:00) ».
Preuves. testOffPeakChargesAtFullPower, testOffPeakFullPowerSwitchesToThreePhases,
testOffPeakFullPowerYieldsToThePhaseLimit ; les tests du lot e1 en heures creuses attendent
désormais le plafond. Sans heure creuse, la preuve decisions est identique.
Lot futur, non implémenté — réserver seulement ce dont la maison a besoin jusqu'au soleil,
d'après la prévision. Aujourd'hui la décharge est bloquée entière tant que la voiture charge
(D1). Une version plus fine laisserait la batterie se décharger dans la voiture au-delà de ce que
la maison consommera d'ici le retour du soleil — consommation de base prévue (BaseloadLog) et
heure de la première production utile (prévision PV). Elle demande une prévision fiable des deux,
et un pilotage de OutWRte plus fin qu'un tout-ou-rien : c'est un lot à part.
8.2 D8 — près de la limite de phase, la consigne se pose (2026-10-25)
Le défaut. Le plafond d'une borne était la place libre au compteur, sans lui rendre ce
qu'elle tire (evPhaseAllowanceW()). Avec un compteur qui voit la borne, sa propre
consommation lui retirait la place au cycle suivant, puis la lui rendait au cycle d'après. Mesuré
en simulation, maison à 2 A par phase, borne 16 A, mono comme tri :
| Limite | Avant (consigne cycle par cycle) | Après |
|---|---|---|
| 25 A | 16, 7, 16, 7… (11,5 A en moyenne) | 16 A tenus |
| 32 A | 16, 14, 16, 14… | 16 A tenus |
| 16 A | 14, 6, 8, 6, 8… (7 A en moyenne) | 13 A tenus, 15 A au compteur |
La règle. En heures creuses, et pour la pleine puissance du lot e2 seulement, le plafond est
lu par EnergyArbitrator::evOffPeakAllowanceW() :
plafond par phase = limite − (tirage de la phase − courant mesuré de la borne) − 1 A, arrondi à l'ampère inférieur, × phases.
C'est la règle de l'échéance chez l'amont (adjustEvChargers()). Elle converge en un cycle :
le résultat ne dépend plus de ce que la borne tirait, seulement de ce que tire le reste de la
maison.
- Mesure, pas consigne. Le courant rendu est l'état
currentPowerde la borne, sur les phases où elle le mesure. Une voiture qui prend 10 A d'une borne commandée à 16 n'occupe que 10 A : lui rendre 16 lui ouvrirait 6 A de trop. Une borne sans mesure, à l'arrêt, ou qui bascule de phases (mesurées ≠ visées) ne rend rien, et la marge s'applique quand même. - La marge : 1 A par phase. C'est celle de l'amont, pour la même raison : laisser à un appareil moyen allumé entre deux cycles la place de démarrer avant que L4 ait à couper. Elle couvre aussi l'écart entre la puissance annoncée par la borne et le courant lu au compteur (tension ≠ 230 V, ±3 % à 16 A, soit 0,5 A). L'arrondi à l'ampère inférieur garantit qu'elle n'est pas mangée par l'adaptateur, qui arrondit la consigne au plus proche. Plus grande, elle coûterait 230 W par phase toute la nuit, sans raison mesurée ; plus petite, elle ne couvrirait plus l'écart de mesure.
- La sécurité en dépassement tient. C'était la raison de ne rien rendre. Si une phase tire
I > limite, dontEpour la borne, la place rendue vautlimite − (I − E) − 1 < E − 1: la consigne baisse d'au moins 1 A par cycle, jusqu'à sortir du dépassement ou jusqu'au plancher, où la stratégie tient la borne et où L4 décide. Cela suppose que le compteur voie la borne — ce que L4 suppose déjà. - Un compteur qui ne voit pas la borne ne la mène pas seule à la limite : le tirage de la maison sans la borne est écrêté à zéro, donc une phase vaut au plus sa limite moins la marge.
- Où. Seulement là où
EvAdaptervise la pleine puissance d'heures creuses (trois modes, heure creuse en cours). Hors heures creuses et sans tarif,evPhaseAllowanceW()reste la règle, et les décisions sans tarif ne changent pas. En heures creuses, une dérogation sur la borne lit le même plafond (c'est le mêmedeclared.maxPowerW). Sans heure creuse, la dérogation garde l'ancienne règle : la faire passer par la même voie changerait son comportement sans tarif, et ce n'est pas décidé.
Hypothèses. Les phases de la borne sont celles du compteur (même hypothèse que L4 et que l'amont). Une borne qui annonce plus que ce que le compteur voit d'elle reçoit au plus 1 A sous la limite. Un compteur rafraîchi après la borne (borne qui monte, compteur pas encore) peut surestimer la place d'un cycle, dans la même limite ; L4, lui, lit le compteur en temps réel.
Preuves. testOffPeakFullPowerSettlesUnderThePhaseLimit (sept cas, douze cycles : la consigne
se pose dès le premier cycle, la phase la plus chargée reste à 1 A au moins sous la limite ; un
cas à 2,4 A de maison prouve l'arrondi) ; testOffPeakFullPowerYieldsToThePhaseLimit, réécrit
avec un compteur qui voit la borne (mesure et non consigne, dépassement, plancher, coupure par
L4) ; testOffPeakFullPowerStaysUnderTheLimitOnABlindMeter (écrêtage à zéro) ;
testOverloadProtectionEcoMode, hors heures creuses, inchangé. Dans
testBlockedDischargeShowsAtTheMeterAndL4Decides, la borne en dépassement finit à son plancher
(L4 la ramène de 16 à 13 A, la stratégie laisse 9 A, L4 la ramène à 6 A) au lieu d'être coupée :
l'assertion attend désormais « plancher ou coupée ».
8.3 D9 — la somme éco + confort tient dans le plafond (2026-10-25, suite)
Le défaut (écart 4). En heures creuses, le niveau éco d'une borne vaut son plafond de cycle
(declared.maxPowerW), 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. Mesuré en simulation : borne
monophasée sur A, limite 16 A, maison à 2 A sur A, phases B et C exportant 1 500 W chacune. Le
plafond vaut 13 A ; la consigne montait à 16 A, et la phase A à 18 A pour 16. Le partage
publiait 2 990 W achetés quand le compteur n'en importait que 450.
La règle. Pour une borne servie à pleine puissance en heures creuses
(EV_OFF_PEAK_FULL_POWER) :
- la passe confort sert le surplus dans le plafond, et non par-dessus ;
- l'achat cède ce que le surplus finance : achat = plafond − part confort, jamais négatif.
La somme reste donc le plafond, et l'achat publié (
gridW,draw.committedW,counts) est ce qui reste à acheter ; - la part confort n'a pas de plancher : le plancher porte sur la somme, qui vaut le plafond, donc
jamais moins que lui. Sans cela, un surplus sous le plancher serait refusé, ou démarré par
EV_GRID_START— un second achat en plus du premier.
Le motif suit la règle de la part majoritaire : à l'aube, quand le surplus finance plus de la
moitié du plafond, la décision publiée est celle du confort ; le niveau éco reste lisible dans
levels. La marge de soutirage du reste de la cascade est décomptée comme avant (prudence
inchangée). Hors heures creuses et sans tarif, rien ne change : la règle ne s'applique qu'aux
bornes qui portent EV_OFF_PEAK_FULL_POWER.
| Cas (simulation) | Avant | Après |
|---|---|---|
| Aube, 2 540 W de surplus | 16 A, phase A à 18 A ; 2 990 W achetés | 13 A, phase A à 15 A ; 450 W achetés, 2 540 W de surplus |
| Plein soleil, surplus > plafond | 16 A | 13 A ; 0 W acheté, 2 990 W de surplus |
| Nuit | 13 A ; 2 990 W achetés | inchangé |
| Compteur aveugle aussi au total | 13, 16, 16… A | 13, 15, 15… A |
Avec un compteur aveugle au total, le surplus est fabriqué par le recrédit : la consigne tient le plafond, mais le partage publié, lui, ne peut pas être juste — le compteur ne dit pas ce qui est acheté.
Preuves. testOffPeakFullPowerAtDawnStaysInTheCeiling (nuit, aube, plein soleil, quatre
cycles chacun : consigne et phase, somme, achat ≤ import du compteur) ;
testOffPeakFullPowerStaysUnderTheLimitOnABlindMeter, étendu au total aveugle. La garde de
bascule de §8.2 est désormais éprouvée : testOffPeakFullPowerGivesNothingBackWhileSwitchingPhases
(borne en monophasé à 10 A qui bascule en triphasé, maison à 12 A sur B, limite 25 A : 12 A, et B
à 24 A ; sans la garde, 15 A et B à 27 A).