§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>
475 lines
33 KiB
Markdown
475 lines
33 KiB
Markdown
# 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
|
||
(preuve `decisions` IDENTIQUE, et `testWithoutOffPeakTheEngineIsEtm74`).
|
||
- 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::tariff` n'est toujours
|
||
pas rempli : l'optimiseur ne reçoit rien de neuf. Les heures creuses vivent dans
|
||
`SurplusContext::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_Mod` est un **champ de bits** — « Activate hold/discharge/charge storage control
|
||
mode » : bit 0 `CHARGE`, bit 1 `DiSCHARGE`. Le bit **active la limite** correspondante, il
|
||
n'« autorise » pas l'opération.
|
||
- `OutWRte` est la limite de décharge, en **pourcentage** de la puissance maximale (`% WDisChaMax`
|
||
dans le modèle, « % de `WChaMax` » 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'échelle `InOutWRte_SF` (que le plugin relit avant chaque écriture, AC-12).
|
||
- L'état `enableDischarging` du plugin **est** ce bit (`readStates()`), et l'action le pose par
|
||
lecture-modification-écriture de `StorCtl_Mod`. L'état `dischargingRate` **est** `OutWRte`.
|
||
- 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 `qCWarning` par série (« … REFUSÉE (…) : la décharge n'est PAS
|
||
bloquée … »), les refus suivants et les nouvelles tentatives en `qCDebug`, et un `qCInfo` à
|
||
l'acceptation qui clôt la série (« … ACCEPTÉE après N refus »).
|
||
- **Télémétrie** : pendant une série, **`held` est faux** — et `dischargeBlocked` avec lui — et
|
||
la batterie publie `refusedWrites` (poses refusées d'affilée), `refusedSince` et
|
||
`nextAttemptAt`. La décision reste `STORAGE_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ÉE` et `… RENDUE à l'automatisme` aux transitions (`qCInfo`), le rafraîchissement
|
||
en `qCDebug`, un refus d'écriture en `qCWarning` (règle 7-d) — une fois par série de refus
|
||
pour la pose (§3.4 bis).
|
||
- **Télémétrie** (`GetLoadTelemetry` et `LoadTelemetryChanged`, même charge utile) : un bloc
|
||
`storage`, **absent** sans heure creuse ou sans batterie — `dischargeBlocked` (ce que le moteur
|
||
TIENT après le dispatch), `decision` `{code, params}`, et `batteries[]` avec ce qui est LU
|
||
(`observed.enableDischarging`, `observed.dischargingRate`) ; pendant une série de refus,
|
||
`held` faux et `refusedWrites`, `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 que `enableDischarging` —
|
||
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 vitrine ? | **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 vitrine ? | **Oui, avant la vitrine.** 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-h` refuse 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 `currentPower` de 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`, dont `E` pour la borne, la place rendue vaut `limite − (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ù `EvAdapter` vise 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ême `declared.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).
|