Patrick Schurig baf267a963 feat(3g-2): une charge, un commandeur
Le relevé machine du 2026-08-26 l'énonçait sans détour : « tant que ceci n'est
pas traité, la transplantation ne commande rien ». L'arbitre décidait,
publiait un motif juste, et adjustEvChargers() commandait autre chose une
milliseconde plus tard. Ce lot ferme les trois points que le banc a ouverts.

1. UNE CHARGE, UN COMMANDEUR

Les actions EV du waterfall sont enfin DISPATCHÉES — elles ne l'étaient pas du
tout : m_loadAdapters ne contient pas les bornes, et applyActionsToAdapters()
s'arrêtait là. Et les bornes ainsi commandées sortent du chemin proxy par deux
crochets virtuels posés dans l'amont, interrogés à deux moments du cycle où la
réponse n'est pas la même : evSurplusPlannedByWaterfall() avant le plan (à qui
la borne appartient-elle), evCommandedByWaterfall() après (qu'a-t-il commandé).
Sans cette distinction, une borne servie par l'échéance ne serait plus
commandée du tout.

Le jeu des bornes commandées est NOMMÉ par le scheduler (Plan::waterfallEvIds).
Le redéduire ailleurs — « motif différent d'EV_DEADLINE », « financement au
surplus » — dériverait du vrai au premier motif ajouté, et EV_GRID_START porte
justement funding=Grid tout en venant du waterfall. Il est vidé à l'entrée du
mode dégradé : le waterfall ne commandant plus, il ne peut plus revendiquer.

chargingState, que posait adjustEvChargers(), est repris par l'arbitre pour
les bornes qui n'y passent plus — sinon l'app afficherait « en charge » une
borne que l'arbitre vient d'éteindre.

2. evFloorW() REND L'EXIGENCE RÉELLE, ET LA TOLÉRANCE DEVIENT VISIBLE

Le plancher valait `min × acquisitionTolerance × 230 × phases` : 2 070 W
annoncés pour une borne qui en exige 4 140. Le waterfall allouait 2 828 W,
l'adaptateur refusait d'enclencher sous 6 A, et 2 828 W étaient retirés du
budget pour personne. Ce sont deux grandeurs différentes : le plancher est ce
que la borne EXIGE, la tolérance est la part qu'on accepte de ne pas couvrir.

La tolérance n'est pas supprimée pour autant — un réglage documenté (défaut
0,5) qui devient inerte sans le dire est un silence ambigu, et tout le parc
verrait son VE démarrer deux fois plus tard sans explication. Elle devient un
seuil de démarrage EXPLICITE : motif EV_GRID_START {budgetW, floorW, gridW},
funding=Grid, et une comptabilité qui se partage — budgetW dans
budget.allocatedW, gridW dans budget.evReservedW, budgetW + gridW ==
allocatedW.

C'est une brèche assumée dans le financement réseau que DESIGN_3g §3.1 déférait
à 3f. Elle est bornée par construction, pas par discipline : surplus réel exigé
(budgetW > 0), tout le budget restant consommé donc AU PLUS UNE borne par
cycle, et le plafond de la protection de surcharge s'applique avant. Écrite en
toutes lettres dans DESIGN_3g §3.1-bis.

3. LE RANG D'UNE BORNE EST CONFIGURABLE

Il vit là où vivent tous les rangs : LoadConfig.priority (LM-1206/LM-1207). Pas
de second système de priorité — un classement propre aux bornes ne saurait pas
exprimer « VE1 > ECS > VE2 », et deux classements qui ne peuvent pas
s'interclasser sont un défaut, pas deux fonctionnalités. ChargingInfo ne porte
donc aucun champ de rang.

Toute borne détectée reçoit d'office une entrée « evcharger », portant le rang
et RIEN d'autre : ses limites viennent du Thing et changent avec la voiture
branchée. Création journalisée, une ligne par borne, à la création seulement —
une écriture que personne n'a demandée doit laisser une trace. Le tri devient
TOTAL : à rang égal, l'identifiant départage, donc laquelle démarre est
reproductible même avant tout réglage.

L'entrée SURVIT à la disparition de son Thing : borne remplacée, ThingId
changé, appareil déposé. Elle reste publiée available: false / faultCode:
THING_MISSING / motif LOAD_UNAVAILABLE, et ne retient aucun budget. La
supprimer d'office rendrait « borne remplacée » indistinguable de « borne
jamais configurée ».

CORRIGE UNE RUPTURE D'INVARIANT DE 3g-1, non signalée à l'époque : en faisant
entrer les bornes dans loads[], 3g-1 publiait des charges qui ne figuraient
dans AUCUNE GetLoadConfig — la charge fantôme que le lot B-bis avait
supprimée, réapparue par l'autre bout. L'app s'appuie sur cet invariant pour
résoudre libellé, domaine et rang. L'auto-provisionnement le rétablit par
construction.

ÉPROUVÉ

  - testEvChargerRankDecidesWhoCharges : deux bornes, un budget qui ne paie
    qu'un plancher, la seconde refusée avec BELOW_MIN_POWER {budgetW: 620,
    minPowerW: 1380}, et l'inversion du rang change la borne servie. C'est la
    moitié ARBITRAGE de LM-1204-b. Couvre aussi l'aller-retour neutre par la
    frontière RPC et l'inclusion loads[] ⊆ GetLoadConfig.
  - testEvGridStartIsAnnouncedAndBounded : le motif chiffre le soutirage, la
    comptabilité se réconcilie, une seule borne soutire par cycle.
  - testEvChargerConfigOutlivesItsThing : l'entrée survit, publiée en défaut.
  - testEvChargerConfigIsRankOnly / RefusesPayloads / ClaimsItsThing.

Deux points de contrôle de `run` bougent, dans un seul scénario et par une
cause unique : la conversion watts→ampères se fait désormais après
l'allocation, et l'arrondi tombe parfois de l'autre côté. 14 des 16 scénarios
sont identiques au bit près, et ce qui compte est inchangé dans les deux —
la voiture est à 100 % à l'échéance.

Suite complète : simulation 27/27, charging 16/16, loadmodel 16/16,
spotmarket 7/7. Build amd64 0/0.

RIEN N'A TOURNÉ SUR MACHINE. docs/RECAP_3g2.md dit ce qui reste à éprouver au
banc — dont LM-1204-b avec deux bornes réelles, la Terra AC étant vérifiée
absente du réseau (ARP INCOMPLETE depuis le banc lui-même).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017TVywtayEeoRJ2yWUW4iZj
2026-08-27 10:43:25 +02:00

5.0 KiB

RECAP — reprise de session, 3g-1

PÉRIMÉ deux fois. Pour l'état du banc : docs/RELEVE_3g1.md. Pour ce qui a été fait des défauts qu'il a révélés : docs/RECAP_3g2.md. Le paragraphe ci-dessous n'est conservé que pour la trace.

PÉRIMÉ le 2026-08-26 au soir. Le paquet a été déployé et le banc a parlé : lire docs/RELEVE_3g1.md, qui remplace ce document. En un mot : la télémétrie ne publiait pas les bornes (corrigé, +etm22), et surtout adjustEvChargers() commande encore les bornes après le waterfall — tant que ce n'est pas traité, la transplantation ne commande rien.

Arrêt : 2026-08-26, contexte saturé. Branche : feature/beta-rulebased. Dernier commit : 1478d8f — feat(3g-1) : les bornes entrent dans le waterfall.


1. État exact à la reprise

Code 3g-1 complet, compile 0 erreur / 0 warning, tests/auto/loadmodel 15/15
Paquet 1.15.2+etm21 construit et sorti du conteneur, dans etm-powersync-deploy/
Déployé sur .75 NON — le banc tourne encore 1.15.2+etm20
Critère LM-1204-b NON exercé — c'est la première chose à faire

Reprendre exactement ici :

# Le paquet est DÉJÀ sorti du conteneur — il est ici, prêt :
cd ~/projects/etm-powersync/etm-powersync-deploy
./deploy.sh --dry-run powersync-energy-plugin-nymea_1.15.2+etm21_arm64.deb   # puis sans --dry-run
# puis : exercer LM-1204-b (specs/spec_loadmodel.md)

Si le fichier a disparu, le reconstruire n'est pas nécessaire — il est encore dans le conteneur : lxc exec build-cross-arm64 -- cat /root/powersync-energy-plugin-nymea_1.15.2+etm21_arm64.deb > <cible>.


2. Ce que 3g-1 contient

Les bornes entrent dans le waterfall. Elles partagent le budget, décrémentent la cascade, et portent un rang qui a enfin un sens.

Le défaut corrigé n'était pas théorique : planSurplusCharging() calculait l'allocation une fois avant la boucle et ne la décrémentait jamais — chaque borne recevait la totalité du surplus. Le rattrapage venait du compteur au cycle suivant, pas de l'arbitrage.

Trois motifs EV supprimés (pas remplacés) : EV_SURPLUS → SURPLUS_SETPOINT, EV_ECO_MIN → le plancher du §12, EV_IDLE → BELOW_MIN_POWER / SURPLUS_INSUFFICIENT.

Restent au proxy, délibérément : échéance de départ, spot market, protection de surcharge, estimation de SoC. evReservedW est restreint au financement réseau, pas supprimé.


3. À vérifier en priorité au redéploiement

3g-1 touche le chemin EV en production sur un banc avec une Terra AC réelle. Rien de tout ceci n'a encore tourné une seule fois.

  1. LM-1204-b — surplus ≈ 4 kW, deux bornes à ≈ 1,4 kW de plancher. Attendu : la première servie à fond, la seconde à 0 si le reliquat est sous son plancher. Discriminant : le motif publié — BELOW_MIN_POWER prouve que l'arbitre a refusé ; une borne à 0 W sans motif de plancher prouve que c'est le matériel, donc que le test ne prouve rien.
  2. Inversion de rang entre les deux bornes → la charge servie doit changer.
  3. loads[] contient-il les bornes, et la somme des allocatedW retombe-t-elle sur budget.allocatedW ? (Ce n'était pas vrai avant 3g-1 — c'est le changement de contrat.)
  4. Le nombre de phases : vérifier que le plancher publié correspond au nombre visé. C'est le piège le plus silencieux du lot.
  5. Aucune double commande : une borne servie par l'échéance ne doit pas recevoir en plus une allocation de surplus (gridFundedEvs).

4. Ce qui reste ouvert

  • §12 / BELOW_MIN_POWER sur le banc : ✅ observé le 2026-08-26 — chauffe-eau 0 W {budgetW: 249, minPowerW: 500} et le budget a cascadé vers la PAC.
  • BATTERY_RESERVE : observé, et corrigé à cette occasion — il était publié avec withheldW: 0 sur un surplus négatif. Conditionné à withheldW > 0 depuis. La correction n'a pas été revérifiée sur machine.
  • Bascule batteryLevelConsideration 0,4 / 0,9 : jamais faite avec une batterie visible. Elle est possible depuis nymea-plugin-sunspec-etm01 1.15.2+etm11, qui déclare enfin energystorage. Le banc est resté à 0,9. À faire dans les deux sens, journaux à l'appui.
  • §10 éco/confort : ne part pas sans l'extension du délestage au waterfall (précondition écrite, specs/spec_loadmodel.md §10).
  • Brief ouvert chez le plugin SunSpec : etm-powersync-plugins-modbus, BRIEF_sunspec_energystorage.md — la partie energystorage est faite ; la question §5 (l'onduleur ne publie aucun cumul, ce qui gèle totalProduction) reste ouverte.

5. Documents à lire à la reprise, dans cet ordre

  1. docs/DESIGN_3g.md — ce qui se transporte et les trois choses qui ne se transportent pas.
  2. specs/spec_loadmodel.md §12 (plancher), LM-1203-b (réserve annulée), LM-1204-b (critère).
  3. docs/RELEVE_LOTD.md — la méthode de relevé attendue : machine / test / lecture, distingués.