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

101 lines
6.0 KiB
Markdown

# `etm-powersync-ref-optimizer` — optimiseur de référence
Python 3, bibliothèque standard seule, GPL-3.0 comme le dépôt. Protocole : `docs/OPTIMIZER_PROTOCOL.md`
1.3 (annexe E). Il répond un plan à **un créneau** qui reproduit le waterfall du rule-based (passe
confort : même ordre, même budget, mêmes écrêtages) en ne lisant **que** le contexte `GetPlan` — aucun
accès réseau sortant, aucun fichier hors le token. Usages : test de bout en bout du moteur, point de
comparaison d'un banc, exemple pour qui écrit son propre optimiseur.
**Nom annoncé au handshake : `powersync-optimizer`** (c'est le `name` que `GetOptimizerStatus`
publie). `powersync-optimizer` = exemple de référence GPL (ce dépôt) ; `etm-optimizer` = optimiseur ETM (propriétaire, hors de ce dépôt). Le fichier et son
répertoire gardent le préfixe des artefacts du dépôt, `etm-powersync-ref-optimizer`.
Ce qu'il ne reproduit pas (le contexte 1.1 ne le porte pas) : passe éco, dérogations, plafond de
soutirage, paliers d'un `relay-router` (il envoie une consigne continue que le routeur arrondit),
refus de commande, proxy VE. L'en-tête du script le détaille.
## Lancer
```sh
# socket local (défaut : unix:///run/powersync/optimizer.sock, mode 0660)
./tools/etm-powersync-ref-optimizer/etm-powersync-ref-optimizer
# sur le LAN, avec un token (lu dans un FICHIER, jamais en argument)
./tools/etm-powersync-ref-optimizer/etm-powersync-ref-optimizer --listen tcp://0.0.0.0:7600 \
--token-file /etc/powersync/optimizer.token
```
Il écrit `ÉCOUTE <endpoint>` sur la sortie standard quand il écoute (le port réel de `tcp://…:0`),
et son journal sur la sortie d'erreur. Plusieurs clients à la fois (un fil par connexion, aucun état
partagé) : le moteur et un rejeu peuvent lui parler ensemble. SIGTERM l'arrête proprement.
## Erreurs (protocole 1.2 et 1.3)
Codes JSON-RPC 2.0, plus `-32001` (protocole §7) :
| Reçu | Réponse | Connexion |
|---|---|---|
| `Handshake` sans le token attendu, ou avec un autre — **même d'une autre majeure** (1.3 : le token d'abord) | `-32001` | fermée |
| `Handshake` d'une autre majeure, token accepté ou non exigé | `-32602` | fermée |
| `GetPlan` avant un handshake réussi | `-32600` | gardée |
| `GetPlan` dont le `timestamp` est absent, n'est pas une chaîne, ou est illisible ou sans décalage | `-32602`, `error.data` = `{"problems":[{"path":"params.timestamp","problem":"missing"\|"type"\|"format"}]}` (forme 1.3, protocole §7), jamais la valeur | gardée |
| ligne non JSON | `-32700`, `id: null` | gardée |
| lot `[…]` | `-32600`, `id: null`, aucun message du lot traité | gardée |
| ligne de plus de 1 Mio (`\n` non compté) | `-32600`, `id: null`, jetée sans être gardée en mémoire | gardée |
| requête d'une autre méthode, dont `UpdateLearned` avec `id` | `-32601` | gardée |
Sans `--token-file`, un token reçu au handshake est accepté sans être lu. Une notification (sans
`id`) est ignorée.
## Pointer le moteur dessus
```sh
# méthodes NymeaEnergy (permission Admin) — INTERFACE.md, « Optimiseur »
NymeaEnergy.SetOptimizerEndpoint {"endpoint": "unix:///run/powersync/optimizer.sock"}
NymeaEnergy.SetOptimizerToken {"token": "<le même secret que dans --token-file>"} # clé seule
NymeaEnergy.GetOptimizerStatus # optimizerAlive, activeStrategy, name, lastErrorCode…
```
Sur `tcp://` sans TLS, le token circule en clair sur le LAN (protocole §3).
## Traces, puis rejeu
```sh
NymeaEnergy.SetOptimizerTrace {"enabled": true} # défaut : désactivé
NymeaEnergy.GetOptimizerTrace # → directory
# une ligne par cycle : <directory>/etm-powersync-optimizer-trace-AAAA-MM-JJ.ndjson
# rejouer une journée sur un optimiseur (celui-ci, ou un candidat)
./tools/etm-powersync-trace-replay <directory>/etm-powersync-optimizer-trace-2026-10-17.ndjson \
--endpoint unix:///run/powersync/optimizer.sock --fail-on-divergence
```
Le rejeu compare, cycle par cycle, les actions du créneau courant au plan enregistré (`tools/README.md`).
## Modes d'injection de défauts (`--mode`, tests)
`normal` · `abstain` · `slow` (répond après `--slow-s`, défaut 12 s, au-delà des 10 s du moteur) ·
`invalid` (`slots` n'est pas une liste) · `expired` (`validUntil` passé) · `wrong-major` (handshake
2.0) · `drop` (ferme la connexion à chaque `GetPlan`) · `unknown-load` · `forbidden-kind` (kind
`constraint`) · `missing-load` (une charge disponible absente du créneau courant) · `echo-token`
(recopie le token dans `reason` et `planId`) · `protocol-1.0` (optimiseur 1.0 : sans champs 1.1) ·
`deviate` (cascade en ordre inverse : doit diverger) · `wrong-type` (`planId` numérique : le moteur
l'accepte, le contrat OpenRPC le refuse — contre-épreuve de la suite de conformité) · `old-data`
(contexte invalide : `error.data` dans la forme d'avant 1.3, `{"problemes": [phrases]}`) ·
`echo-value` (contexte invalide : `error.data` recopie la valeur reçue) · `major-first` (la majeure
jugée avant le token) · `over-limit` (toute consigne en watts portée à 50 kW, au-delà des bornes
de la charge et du branchement : le moteur doit l'écrêter).
`--announce VERSION` : la version annoncée au handshake, et dont la majeure est exigée du moteur
(défaut `1.3`). Avec `--announce 1.2`, il se présente en optimiseur 1.2 : la suite de conformité
n'exige plus de lui la forme 1.3 d'`error.data` ni l'ordre token puis majeure, elle avertit.
`--mode-file FICHIER` : le mode est relu dans ce fichier à chaque message — un test change de mode
sans reconnexion. `--capture FICHIER` : chaque message reçu et envoyé, une ligne JSON-RPC par
message — la session que `tools/etm-powersync-schema-check --ndjson` juge contre le contrat ; le
token y devient autant d'astérisques (forme gardée, valeur jamais écrite). `--list-codes` : le catalogue des codes émissibles qu'il embarque (le test
d'intégration le confronte au moteur).
Tests : `tests/auto/refoptimizer` (suite `etm-powersync-refoptimizer`, moteur réel + ce script en
sous-processus).