Files
EVCS/HANDOFF.md
Tallagrand Thierry d1efeac44c R3: suite de tests golden — 16 scénarios, bloquante avant push
- tests/golden/run.mjs : chaîne complète des 5 nodes (code réel) en sandbox vm,
  horloge simulée par scénario, snapshot HA réel comme fixture.
- 16 scénarios attendus figés après revue : nuit HC (P7), solaire, abondance P6,
  provocation P3, gardes SOC, force_grid/force_charge, stop, overload.
- n8n-push.mjs exécute la suite avant tout PUT du workflow EVCS :
  golden rouge = push refusé (--skip-golden en échappatoire consciente).
- Validé par mutation : P7 cassé volontairement -> 2 échecs détectés + push bloqué.
- tools/fetch-snapshot-fixture.mjs : recapture d'un snapshot production.
2026-08-07 19:16:03 +02:00

575 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HANDOFF.md — EVCS / Grid Hysteresis — État au 09/07/2026
## 1. Contexte Projet
- **Installation** : Victron ESS **sans injection réseau**. Batterie 17,5 kWh utile.
- **Workflows n8n actifs** :
- `HA Snapshot EVCS Surplus Controller V2.6` (id `ATswvGfG4KJUls0K`) — pilote la borne EVCS (30 s)
- `Victron - Battery / Grid Hysteresis Fix V1` (id `7iIlu7LLTW7TZA8o`) — pilote le switch EDF (1 min)
- **Accès** :
- HA : `http://10.10.10.17:8123` — jeton dans `jeton_EVCS.txt` (⚠️ en clair, à révoquer si dépôt partagé)
- n8n : `https://n8n.airtria.pro` — clé API dans `~/.config/opencode/opencode.json` (mcp.n8n-mcp)
- **MCP opencode** (option 1 déployée) : 5 serveurs distants dans `~/.config/opencode/opencode.json`
(`ha-get-state`, `ha-set-helper`, `grid-set-state`, `victron-set-charge-limits`, `pv-get-forecast`)
→ endpoints SSE `https://n8n.airtria.pro/mcp/<path>/sse`.
Les 4 outils paramétrés utilisent `$fromAI(...)` (corrigé le 09/07).
⚠️ **Endpoints MCP n8n sans authentification** — à sécuriser (Bearer sur les nodes MCP Server Trigger).
- **Fichiers de référence locaux** :
- `WINDOWS/WINDOW_SOURCEV2.js`, `BATTERY/BATTERY_CLOUD_LOCKS.js`, `HYSTERESIS/COMPUTE.js` — copies de lecture (le code qui fait foi est dans n8n)
- `modified/GRID_HYSTERESIS_backup_20260709-*.json` — rollback avant P1
- `modified/EVCS_V2.6_backup_20260709-*.json` — rollbacks avant P2 puis P3 (2 fichiers)
## 2. Architecture Data-Flow
```
EVCS V2.6 (30s) :
HA Snapshot (template) → INPUT_PARSE → WINDOWS_SOURCEV2
→ BATTERY_CLOUD_LOCKS → DECISION_ENGINEV2 → HA_SERVICE_CALLS_OUTPUT
→ Build Actions → Execute HA Service
Grid Hysteresis (1min) :
HA Snapshot → Compute Grid Hysteresis → Build Actions → Execute HA Service
```
Communication inter-workflows via helpers HA :
`input_text.victron_evcs_source_mode` (labels FR), `input_boolean.victron_evcs_night_grid_takeover_needed`,
`input_text.victron_evcs_surplus_state`, `input_select.victron_evcs_strategy`.
## 3. Patchs déployés le 09/07/2026 (session opencode)
### P1 — Grid Hysteresis (déployé, vérifié)
1. **Entité fantôme corrigée** : le snapshot lisait `input_select.victron_evcs_user_strategy`
(inexistante → toujours `unknown`). Corrigé en `input_select.victron_evcs_strategy`.
Conséquence avant fix : les overrides `force_grid_now`/`force_charge_now` et la coupure
« voiture pleine » ne fonctionnaient JAMAIS dans l'hystérésis.
2. **Switch EDF `unavailable`** : nouvel état `switch_unavailable` = aucun ordre, aucun timestamp,
réévaluation complète au retour du switch. Avant : spam `turn_on` chaque minute + rallumage
aveugle à la reconnexion (incident du 08-09/07 : disjoncteur physique coupé par l'utilisateur,
EDF rallumé à 00:41 au retour, batterie chargée sur réseau à SOC 48 %).
3. **`last_on_at`** : réécrit uniquement sur vraie transition (latch OFF→ON), plus à chaque retry.
Avant : `hold_on_min_runtime` pouvait durer éternellement.
4. **Bug latent corrigé** : le bloc `turn_off_force_grid_vehicle_full` (V2.5) était écrasé par la
chaîne principale (code mort). Flag `forcedCut` ajouté.
### P2 — EVCS V2.6 : continuité takeover nuit / anti-créneaux (déployé, vérifié en réel)
- Le snapshot relit `night_takeover_prev` (= helper takeover du cycle précédent).
- `nightTakeoverLatched` dans `WINDOWS_SOURCEV2` **et** `BATTERY_CLOUD_LOCKS` :
une fois le réseau HC engagé en session nuit, on y reste jusqu'à fin de session
(fenêtre/débranchement) ou SOC ≥ 90 (`NIGHT_TAKEOVER_RELEASE_SOC_PCT`).
- Supprime le cycle « château fort » : batterie→jeep→seuil 20 %→EDF ON→30 %→EDF OFF→batterie...
- Vérifié en production dans la nuit du 09/07 : takeover engagé à SOC 35 %, EDF resté ON,
Jeep en charge continue (`hold_min_run`).
### P3 — EVCS V2.6 : provocation PV batteries pleines (déployé, dry-run complet)
- Problème (ancien sujet de ce HANDOFF) : sans injection, batteries pleines ⇒ MPPT bridé à la
conso maison ⇒ aucun seuil PV observable ; en plus `forecastBufferKwh` (10,5 kWh véhicule
soustraits) toujours ≤ 0 l'après-midi, et `cloudDebt` en deadlock (batterie pleine = courant
nul = dette jamais remboursée). Sonde afternoon inopérante (`probe_enabled = off` + seuil PV 1000 W).
- Fix : `pvProvocationEligible = SOC ≥ 90 && batterie non déchargée` → `solar_potential`
(intégré à `isHighBatteryForPotential`, donc actif sur les 4 profils jour). Gardes aval intactes.
- **Attention réglage** : `soc_start_min` (HA) = 94 ⇒ plancher de démarrage hors fenêtre.
Le déclenchement pratique est donc SOC ≥ 94 %. Baisser ce helper à 90 pour aligner.
### P4 — Maintien SOC adapté au forecast pendant le takeover nuit (déployé 09/07 après-midi)
- **Découverte clé** : `Victron - Auto Charge Manager V1.2` (id `cTeeHy4ePiVeELLh`) est le
propriétaire des limites DVCC (`MaxChargeCurrent`/`MaxChargeVoltage` via MQTT,
feedback : `sensor.victron_max_charge_current_setting`). Son mode « nuit idle » laissait
30 A de charge batterie dès que EDF était ON → batterie rechargée à fond pendant un takeover.
- **ACM patché** : nouvelle branche prioritaire `evcs_takeover_hold` / `evcs_takeover_recover` :
si `night_grid_takeover_needed` ON + EDF ON → SOC < cible-1 : recharge 30 A ; sinon **maintien
2 A / 50.1 V** (maison + VE sur HC, batterie en pause). Cible lue depuis
`input_number.victron_evcs_night_target_soc` (écrite par le workflow EVCS : forecast fort→35 %,
moyen→55 %, faible→85 % — donc été ~35, hiver ~85 comme demandé).
- **EVCS patché** : avant minuit (20h→8h), la cible matin utilise
`max(today_remaining, tomorrow_kwh)` (sinon today_remaining≈0 à 23h → cible 85 % à tort en été).
Ajout `tomorrow_kwh` au snapshot + `parsed.tomorrowKwh`.
- Dry-runs : 8 scénarios ACM + 2 scénarios cible EVCS validés (voir session 09/07).
### Autres réalisations du 09/07 (après-midi)
- **Borne fantôme résolue** : firmware EVCS 2.05 → **2.08** (session repartie de zéro,
détection véhicule correcte). Les 19 kWh affichés étaient un cumul de session non réinitialisé.
- **Dashboard « EDF Urgence »** créé via API HA (sidebar) : scripts `script.edf_urgence_off`
(hystérésis OFF + latch OFF + stratégie stop + switch OFF — rien ne rallume),
`script.edf_force_on`, `script.edf_rendre_la_main` + cartes d'état et « pourquoi ».
- **Incident Synology** : NAS éteint dans la nuit (cause à chercher dans les logs DSM) →
n8n + HA morts → EDF resté ON toute la nuit, batterie chargée à 96 % sur réseau.
⚠️ SPOF structurel : tout le contrôle vit sur le Synology.
### P5 — Plafond courant EVCS configurable quand EDF est ON (déployé 09/07 soir)
- Le plafond 20 A venait de `cfg.evcsDedicatedMaxW` (4900−300 W / 230 V) codé en dur dans INPUT_PARSE.
- Nouveau helper **`input_number.evcs_plafond_courant_edf_on`** (6–32 A, défaut 20) : quand
**AC-IN est physiquement présent** (`acin1_available`, pas juste le switch), le plafond = helper ;
hors réseau, retour au plafond câblé ~20 A. Dry-run 5 scénarios OK (dont disjoncteur coupé).
- Subtilité : `victron_evcs_current_max_auto_a` (22) plafonne EN PLUS les modes auto ;
en force_grid/force_charge seul le nouveau helper compte.
- **Boutons dashboards** (EVCS Master = fichier YAML `/mnt/ARCH3/docker/HA/dashboards/evcs_master.yaml`
édité via NFS ; EDF Urgence = API) : carte « Plafonds courant EVCS » + boutons
**⚡ BOOST EDF 28A** (`script.evcs_boost_edf` : plafonds 28/28 + force_grid_now + consigne 28)
et **Retour normal** (`script.evcs_boost_off` : 20/22 + auto_solar_plus).
- Sémantique clarifiée : « Courant demandé » = « Consigne envoyée » = `number.victron_evcs_set_current_command`
(réécrit toutes les 30 s par le workflow) ; « Consigne lue » = readback borne. En force_charge_now,
PV ≥ 700 W ⇒ source « Solaire » ⇒ gardes nuages/batterie freinent : pour une grosse charge, force_grid_now.
### P6 — Override « abondance » du plancher SOC de démarrage (déployé 12/07 midi)
- **Symptôme** : 12/07 midi, PV 8,9 kW, batterie en charge +125 A, Jeep branchée → EVCS bloqué
`idle_wait_conditions`. Cause unique (dry-run) : plancher SOC fenêtre midi
(`input_number.victron_evcs_midday_soc_min` = 75) > SOC 66.
- **Fix (`BATTERY_CLOUD_LOCKS`)** : si `pvPowerAvg10m ≥ 4000 W` **et** `batteryCurrentA ≥ +20 A`
(abondance physique : la batterie monte même avec l'EVCS), plancher de démarrage abaissé à
**40 %** (`effectiveSocStartFloor`). Jours nuageux : planchers d'origine intacts.
Debug exporté : `solarAbundance`, `effectiveSocStartFloor` (safeguards).
- Vérifié en réel : déploiement 12:43 → `start_min_current` immédiat → charge 12 A.
- **Observation borne** : après des heures branchée en statut 6, la Jeep a mis ~2,5 min à
s'engager ; le workflow renvoie `turn_on` toutes les 30 s (retry intégré suffisant).
- Planchers de référence : matin 35, midi 75, soir ?, hors fenêtre 94 (`soc_start_min`).
### Maintenance du 09/07 soir (analyse des messages d'erreurs)
- **W-105 borne** (surcharge) : survenu en charge 20 A sur onduleur seul (disjoncteur EDF coupé).
Déjà couvert : gardes vebus_overload + P5 (plafond retombe à 20 A si AC-IN absent).
- **`saveDataErrorExecution: all`** activé sur les 8 workflows critiques (diagnostic des
prochaines erreurs avec données des nodes ; Water Heater V2.3 avait erré 4× le 09/07 20:20-20:35,
cause inconnue faute de données, rétabli seul).
- **Garage Thermal** : la prise `switch.clim_hygro_socket_1` est débranchée volontairement par
moments → l'appel au switch est maintenant conditionné à sa disponibilité (`execute: switchAvailable`).
Plus de warnings « missing entity » toutes les 10 min. Backup `modified/GARAGE_backup_*`.
- **Templates MQTT durcis** (33 occurrences, `configuration.yaml` + `packages/victron_vebus_mqtt.yaml`) :
garde `value_json is defined` → plus d'erreurs `value_json is undefined` aux reconnexions MQTT.
Backups `SAVE_CONFIG/*.bak.20260709-234800`, `check_config` valid, `mqtt.reload` fait.
⚠️ Après un `mqtt.reload`, les sensors restent `unknown` jusqu'à republication Venus :
publier `R/c0619ab34ac5/keepalive` (payload vide) force la republication complète.
## 4. Root Cause de l'incident « EDF ON à 48 % sans charge Jeep » (nuit 08→09/07)
Chaîne complète reconstituée via l'historique HA :
1. Borne fantôme au matin du 08/07 (« véhicule en charge » sans véhicule) + `force_grid_now`
de la veille ⇒ EDF maintenu ON (la coupure voiture-pleine était du code mort, cf. P1.1/P1.4).
2. L'utilisateur coupe le **disjoncteur physique** ⇒ switch `unavailable` pendant ~5 h.
3. Le workflow reste coincé en `hold_on_min_runtime` (P1.3) en spammant `turn_on` (P1.2).
4. 00:40:54 : le switch revient ⇒ 00:41:03 EDF ON sans cause actuelle ⇒ Multiplus charge la
batterie sur réseau (2980 W) ⇒ coupé à 01:06 (min_on 25 min, SOC ≥ 30).
Tous les maillons sont corrigés par P1.
## 5. Sémantique utile (pour ne pas re-chercher)
- **Latch** (`input_boolean.victron_grid_hysteresis_latch`) : « c'est l'automate qui a allumé EDF
et il défend cet état ». Reprise en main manuelle : stratégie `stop` + hystérésis OFF + latch OFF.
- **Fenêtres EVCS (helpers HA, corrigées/vérifiées le 09/07)** : `night_hc` **01:24→07:24** (HC réelles),
`morning_solar` 07:30→11:00, `midday_hc_or_solar` **12:24→14:24** (HC réelles),
`evening_high_soc` 17:00→20:30, sinon `outside_window`.
L'Auto Charge Manager utilisait des fenêtres codées en dur légèrement fausses
(01:30-07:15 / 12:25-14:25) — alignées sur 01:24-07:24 / 12:24-14:24 le 09/07.
- **Seuils hystérésis réseau (HA, valeurs réelles)** : ON ≤ 20 %, OFF ≥ 30 %, min_on 25 min.
- **Cibles nuit** : hard floor 22 % ; cible matin selon forecast : ≥18 kWh→35 %, ≥10→55 %, sinon 85 %.
`nightSessionActive = fenêtre nuit && véhicule branché` (⚠️ pas besoin de charge active).
- **Statut borne 6** = branchée en attente de démarrage (« Inconnu (6) » sur le dashboard).
- **Labels FR** dans `source_mode` : `Réseau forcé`, `Réseau HC autorisé`, `Assistance batterie nuit`,
`Bloqué`, `Solaire`, `Potentiel solaire` — l'hystérésis matche ces labels.
## 6. Méthode de travail validée (à réutiliser)
1. Backup : `GET /api/v1/workflows/<id>` → `modified/*_backup_<stamp>.json`.
2. Patch des `jsCode` par remplacement exact (script node), `node --check` sur chaque node.
3. **Dry-run local de la chaîne complète** : snapshot réel via `POST /api/template` HA,
exécution séquentielle des 5 nodes avec shim `$input`, horloge simulable en surchargeant
`global.Date` (voir session du 09/07).
4. `PUT /api/v1/workflows/<id>` (⚠️ filtrer `settings` : seuls
`executionOrder/timezone/save*/executionTimeout/errorWorkflow` sont acceptés).
5. Vérification live : helpers HA 40-70 s après.
## 7. Backlog / Point du 10/07 au réveil
1. **Vérifier la nuit de test P2+P4** (fenêtre HC 01:24→07:24, forecast fort → cible 35 %) :
- takeover engagé quand SOC ≤ 35 → EDF ON **continu** (pas de créneaux) ;
- `input_text.victron_charge_mode` = `evcs_takeover_recover` puis **`evcs_takeover_hold`** ;
- `sensor.victron_max_charge_current_setting` = **2** pendant le hold (batterie tenue ~35 %) ;
- 07:24 : fin de fenêtre → EDF coupé (`turn_off_recovered`), ACM repasse en profil jour.
Historique utile : `switch.edf_general_ok_switch`, `sensor.victron_battery_soc`,
`input_text.victron_charge_mode` entre 01:00 et 08:00.
2. **Cause de l'extinction du Synology** (DSM → Centre des journaux) + réflexion SPOF :
le fail-safe physique actuel est « EDF ON + charge batterie » quand le NAS meurt.
3. Baisser `input_number.victron_evcs_soc_start_min` 94 → 90 (aligner avec P3) si souhaité.
4. **Sécuriser** : auth Bearer sur les 5 MCP Server Triggers n8n + révoquer/déplacer le jeton HA.
5. **À investiguer** : « conditions de démarrage nuit batterie non réunies » quand la batterie
suffit pour la nuit (conditions de départ `battery_assist_night`).
6. Optionnel : brancher un keepalive/republish Venus automatique après `mqtt.reload`
(cf. note maintenance §3).
## 8. Historique des sessions
- **12/06/2026** : analyse blocage outside_window, déploiement afternoon_probe (V2.6),
fix clignotement EDF (`edf_general_ok_on` conditionné au sourceMode).
- **09/07/2026 (nuit)** : incident EDF nocturne analysé (root cause complète), MCP option 1 en place,
P1 (hystérésis robuste + entité stratégie), P2 (anti-créneaux nuit), P3 (provocation PV).
- **09/07/2026 (journée/soir)** : incident Synology (NAS éteint → EDF ON toute la nuit, batterie 96 %),
firmware borne 2.08 (fantôme résolu), dashboard EDF Urgence + boost 28 A, P4 (maintien SOC
au forecast pendant takeover, via Auto Charge Manager), P5 (plafond EDF ON configurable),
fenêtres HC corrigées (01:24-07:24 / 12:24-14:24), maintenance erreurs (save error executions,
prise garage conditionnelle, 33 templates MQTT durcis).
- **12/07/2026** : P6 (override abondance du plancher SOC midi — PV fort + batterie en charge
⇒ démarrage dès SOC 40) ; validation en réel à 12:43, charge Jeep engagée.
- **17/07/2026 (après-midi)** : **Session de débogage majeure** — résolution d'un enchaînement de bugs
bloquants empêchant la charge solaire de la Jeep :
**Bug 1 — Typo critique dans DECISION_ENGINEV2** :
- Ligne ~333 : `noDemandnightSessionNeedsClearScoreOff` (variable inexistante) au lieu de `noDemandScoreOff`
- Conséquence : `evcsVehicleNoDemandSemantic` restait `false` même quand la voiture était pleine
- Le filtre "véhicule plein/pause" ne fonctionnait jamais → blocage du système
**Bug 2 — Artefacts de corruption dans WINDOWS_SOURCEV2** :
- 3 occurrences de `tool_f6d3316d2001k4aAfEjbKGNUX2` (résidu de copier-coller ou debug)
- Provoquaient des erreurs `is not defined` à l'exécution
- Workflow en ERROR continu depuis 14:49
**Bug 3 — Session nuit jamais clearée** :
- `input_boolean.victron_evcs_night_session_active` restait `on` depuis 23:24 la veille
- Même après la fenêtre HC (01:24-07:24), le profil restait `night_hc` → `battery_assist_night`
- En plein jour avec grand soleil, le système pensait être en "assistance batterie nuit"
- Clearé manuellement à 14:45
**Bug 4 — EDF rebranché automatiquement en journée** :
- Condition `edf_general_ok_on` déclenchée dès `sourceMode` = `grid_forced` ou `grid_hc_allowed`
- Même en plein jour avec du soleil, EDF se rebranchait si le workflow passait en ces modes
- Correction : ajout de `context.profile === 'night_hc'` pour ne rebrancher que la nuit
**Configuration ajustée** :
- `battery_discharge_soft_a` : 0A (au lieu de -8A) — permet le démarrage sans décharge batterie
- `battery_neutral_band_a` : 0A (au lieu de -2A) — tolérance nulle
- `soc_start_min` : 80% (au lieu de 94%) — seuil de démarrage plus réaliste
- `night_target_soc` : 70% (au lieu de 85%) — laisse plus de marge pour le PV
**Mode sonde activé** :
- `probe_enabled` : ON — permet le démarrage à 6A pour "provoquer" la production PV
- Particulièrement utile en autoconsommation sans injection où le PV ne produit que si chargé
**Résultat** :
- Workflow réparé et fonctionnel
- Jeep en charge à 17A avec le soleil (16:58)
- EDF OFF (ne se rebranche que la nuit)
- Système 100% auto en `auto_solar_plus`
- DeliveryRatio à 99.9% — charge stable et efficace
**À noter** : Le courant de sonde (`probeCurrentA`) est codé en dur dans le workflow n8n (nœud SETUP/PARAMS),
pas dans un helper HA. Pour le changer de 6A à 10A, modifier directement dans l'interface n8n.
Mais le workflow augmente automatiquement le courant (17A atteints), donc ce n'est pas critique.
## ⚙️ CONFIG LOCALE — Qwen3.6-35B sur DGX Spark (≠ gros modèle cloud)
> ⚠️ Cette config est CALIBRÉE pour le modèle LOCAL Qwen3.6-35B-A3B (3B actifs,
> contexte limité). NE PAS appliquer telle quelle à un gros modèle cloud
> (FABLE/Claude), qui n'a besoin ni du bornage strict ni du découpage par nœud.
### Infra (validée le 16/07/2026)
- DGX Spark = 10.10.10.8, endpoint vLLM unique sur port 8000.
- UN SEUL modèle à la fois (contrainte NVRAM 1 Spark) → swap via scripts :
- `use-code` : docker compose down (VL) + sparkrun run qwen3.6-35b sur :8000
- `use-vl` : sparkrun stop --all + docker compose up (Qwen3-VL, vision)
- Recette : @official/qwen3.6-35b-a3b-fp8-mtp-vllm --tp 1 --port 8000
--gpu-mem 0.8 --served-model-name qwen3.6-35b
(MTP + flashinfer + tool-parser qwen3_coder déjà inclus dans la recette).
- Qwen3-VL (docker /opt/vllm) réservé à l'analyse de captures d'écran.
### opencode.json (poste Ubuntu, PAS la Spark)
- baseURL: http://10.10.10.8:8000/v1, apiKey "EMPTY".
- Modèle "qwen3.6-35b" : options { temperature: 0.15, reasoningEffort: "none" },
cost tout à 0 (sinon compteur fantôme FABLE), limit context 65536.
- permission.bash : "curl *": "deny", "wget *": "deny" (bloque hallucinations HTTP).
- Les 6 MCP (n8n-mcp, ha-get-state, ha-set-helper, grid-set-state,
victron-set-charge-limits, pv-get-forecast) : NE JAMAIS toucher ce bloc.
### RÈGLES DE TRAVAIL spécifiques au local (dans AGENTS.md)
- Contexte trop petit pour un workflow entier (~357K = ~102K tokens > 65K).
=> JAMAIS charger le workflow complet. UN nœud / UNE tâche à la fois.
- n8n-mcp_get_workflow renvoie le workflow entier => opencode l'écrit dans
~/.local/share/opencode/tool-output/tool_* (tronqué). Le fichier commence par
"Retrieved workflow: <nom>" AVANT le JSON => json.load ÉCHOUE.
Extraction validée : grep -n '"name":"NOEUD"' <fichier> puis sed -n 'X,Yp'.
(ripgrep/rg N'EST PAS installé sur la Spark, utiliser grep.)
- reasoningEffort none coupe le thinking (le tag /no_think en tête de chat NE
marche pas : opencode l'intercepte comme slash-command).
- Skill n8n installée dans .opencode/skills/ — surveiller les compactions.
### PÉRIMÈTRE (le plus important)
- LOCAL Qwen3.6 = tâches CIBLÉES : lire 1 nœud, repérer une faille logique,
patch simple, dry-run. Rapide, gratuit, fiable SI bien borné.
- CLOUD (FABLE/gros modèle) = refontes multi-nœuds, analyse d'un workflow
ENTIER, orchestration MCP longue. Ne pas forcer le local dessus (il boucle).
## 🧩 SKILLS n8n — validées le 16/07/2026 (Qwen3.6 local, PAS gros modèle)
> ⚠️ Config calibrée pour Qwen3.6-35B-A3B local (contexte 65K). Les skills
> aident au tool-calling n8n. Sur gros modèle cloud, elles sont facultatives.
### Installation
- Dépôt : github.com/viliamvolosv/n8n-opencode-skill
- Emplacement OBLIGATOIRE : .opencode/skills/<nom-skill>/SKILL.md
(dossier "skills" PLURIEL ; le repo clone dans un sous-dossier "skill"
SINGULIER → il FAUT remonter : mv n8n-opencode-skill/skill/* .)
- opencode doit être lancé DEPUIS le projet :
cd /mnt/projects/EVCS/EVCS && opencode
(sinon .opencode/skills/ n'est pas découvert → skills invisibles).
- Chargement PROGRESSIF : au repos les 8 skills ne coûtent que ~12K tokens
(18%). Le SKILL.md complet (~16K) ne se charge QUE si la skill est invoquée.
### Skills ACTIVES (5, validées par test réel sur DECISION_ENGINEV2)
- n8n-mcp-tools-expert → récup config nœud via MCP (fiabilise les 6 MCP)
- n8n-code-javascript → analyse/patch du jsCode (vars in/out, utilitaires)
- n8n-expression-syntax → vérif syntaxe {{ }}
- n8n-validation-expert → refs de nœuds, cohérence types, conditions
- n8n-node-configuration → conformité params/mode/position
### Skills DÉSACTIVÉES (déplacées dans .opencode/skills-off/)
- customize-opencode (méta, hors sujet EVCS)
- n8n-code-python (on fait du JS)
- n8n-workflow-patterns (exige le workflow ENTIER → risque contexte)
→ réactivables en les remettant dans skills/ , ou à réserver au cloud.
### Résultat du test de validation (5 skills, DECISION_ENGINEV2)
- Les 5 skills invoquées et nommées correctement, analyse d'ingénieur juste
(16 vars, 7 derived, 22 conditions, pipeline complet reconstitué).
- UNE SEULE compaction (pic 88% au parsing du workflow 357K, reprise propre).
- 2 AttributeError Python transitoires (structure connexions n8n) → auto-corrigés.
- Coût : $0.00.
### RÈGLE contexte associée (dans AGENTS.md)
- Analyse pipeline multi-nœuds : grep -n "name"/"jsCode" + sed sur la plage,
JAMAIS le workflow JSON entier en mémoire Python. Traite 1 nœud, résume, libère.
- Le fichier tool-output commence par "Retrieved workflow: <nom>" AVANT le JSON
→ parade validée : json.loads(text[text.index('{'):]).
### PÉRIMÈTRE (rappel)
- Local Qwen3.6 + skills = analyse ciblée 1 nœud, validation, patch simple. ✅
- Cloud (FABLE/gros modèle) = refonte multi-nœuds massive, workflow entier.
## 9. Session du 18/07/2026 (matin)
### Corrections documentation (MANUEL_COMPLET.md)
- **outside_window** : décrit comme "Mode probe (sonde) pour relancer le PV si besoin" (pas bloquant)
- **Cloud Debt & Probe** : nouvelle section 11.4 complète avec explication dette nuageuse, mode probe 10A, bypass en auto_solar_plus / force_charge_now
- **Stratégies** : clarifié `force_grid_now` = préservation batterie (SOC<22% → recover 35%/50%/75%) vs `force_charge_now` = secours manuel
- **Heure de départ** : nouvelle section 11.5 "Logique Heure de départ (Night EVCS)"
### Modification workflow
- **probeCurrentA** : augmenté de 6A à 10A dans le nœud INPUT_PARSE (fallback)
- Rationale : 6A trop faible pour "provoquer" la production PV, 10A plus efficace
### Versionnage Git
- Export du workflow dans `workflows/evcs-surplus-controller-v2.6.json`
- Commit & push effectués
## 10. Session du 04/08/2026 (matin) — Fix grid HC 1h30 + libération takeover
### Problèmes诊断qués
1. **Charge HC ne démarre pas à 1h30** : Le grid ne se mettait pas en route à l'ouverture de la fenêtre HC
2. **Grid ne rend pas la main après charge** : EDF restait ON après la fin de charge (SOC 71% < 90%)
### Root Causes identifiées
1. **`gridAllowedBySoc`** (BATTERY_CLOUD_LOCKS) : autorise le grid seulement si `soc <= 35%` ou `forceGridRequested`. En profile `night_hc` avec SOC > 35%, le grid était bloqué.
2. **`nightTakeoverLatched`** (WINDOWS_SOURCEV2) : reste latché tant que `soc < 90%` (hardcodé). Pas de condition "véhicule chargé" → le takeover ne se libérait jamais.
### Corrections appliquées
#### Fix 1: `gridAllowedBySoc` — Autoriser le grid en profile night_hc
**Node:** `BATTERY_CLOUD_LOCKS`
```javascript
// AVANT:
const gridAllowedBySoc = parsed.soc <= 35 || strategy.forceGridRequested;
// APRÈS:
const gridAllowedBySoc =
strategy.forceGridRequested ||
context.profile === 'night_hc' ||
parsed.soc <= 35;
```
#### Fix 2: `nightTakeoverLatched` — Libérer si charge terminée
**Node:** `WINDOWS_SOURCEV2`
```javascript
// AVANT:
const nightTakeoverLatched =
nightSessionActive &&
nightTakeoverPrev &&
parsed.soc < NIGHT_TAKEOVER_RELEASE_SOC_PCT;
// APRÈS:
const vehicleChargeCompleted =
!parsed.evcsChargingSwitchOn &&
parsed.soc >= 50;
const nightTakeoverLatched =
nightSessionActive &&
nightTakeoverPrev &&
(
parsed.soc < NIGHT_TAKEOVER_RELEASE_SOC_PCT ||
vehicleChargeCompleted
);
```
### Méthode de déploiement
- **MCP n8n-mcp_update_workflow** : Erreur 1003 (request/body must NOT have additional properties) — bug du MCP
- **Bypass** : Script `tools/n8n-rest-update-test.mjs` créé, utilise REST API n8n directement avec token
- PUT réussi avec body minimal : `{name, nodes, connections, settings, staticData}`
- Settings doit être minimal : `{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner"}`
### Backup
- `backup-workflow-ATswvGfG4KJUls0K-1785849621547.json`
### Vérification
- Les 2 patches sont dans le workflow en production
- À surveiller : prochaine nuit HC (01:24-07:24) pour valider le comportement
---
*Document mis à jour le : 04/08/2026 (~10:00) — Fix gridAllowedBySoc + nightTakeoverLatched via REST API*
*État live à la clôture : Workflow patché, à tester en conditions réelles*
## 11. Session du 07/08/2026 — P7 : Charge HC nuit automatique (VW hybride) + outillage push
### Problème (nuits du 05-07/08, observé sur l'historique HA)
- VW hybride branchée le soir + stratégie `auto_solar_plus` → **aucune charge en HC**.
01:24 : `idle_wait_conditions` / « Conditions de démarrage nuit batterie non réunies »
(SOC 53 %, target=trigger=35 %). L'utilisateur devait intervenir manuellement
(force_charge_now, ou BOOST EDF 28A à 05:50 le 07/08).
### Root causes
1. **`battery_assist_night` ne démarre jamais** : `batteryAssistNightStartOk` exige
`batteryCurrentA >= profileBatteryDischargeFloorA` (= `battery_discharge_soft_a` = 0 A).
La nuit, la maison tire sur la batterie (~ −14 A) → condition impossible.
→ le système attendait que le SOC dérive jusqu'au trigger 35 % (des heures) avant le grid.
(= backlog §7.5 « conditions de démarrage nuit batterie non réunies », résolu.)
2. **`allow_grid_hc_night` (toggle « Réseau HC nuit ») était décoratif** : parsé dans
INPUT_PARSE (`params.allowGridHcNight`) mais utilisé nulle part
(contrairement à `allowGridHcMidday`).
### Fix P7 (déployé 07/08 ~15:45, dry-run 7 scénarios validés)
Dans **WINDOWS_SOURCEV2** et **BATTERY_CLOUD_LOCKS** (logique nuit dupliquée) :
```javascript
const nightGridHcPreferred =
nightSessionActive && // fenêtre HC nuit + véhicule branché
params.allowGridHcNight; // toggle "Réseau HC nuit" ON
```
- `batteryAssistAllowed` : ajout `&& !nightGridHcPreferred`
- `nightGridTakeoverNeeded` : ajout `nightGridHcPreferred ||` dans le OR
**Effet** : dès l'ouverture de la fenêtre HC (01:24) avec véhicule branché,
`nightPreferredSourceMode` = `grid_hc_allowed` (ou `grid_forced` si SOC < cible matin)
→ `edf_general_ok_on` allume EDF → takeover helper ON → ACM `evcs_takeover_hold`
(batterie tenue 2 A) → `gridStartOk` → `start_grid_hc_min_current` puis step-up.
- Toggle « Réseau HC nuit » OFF → comportement historique (battery assist) intact.
- Jour / pas de véhicule / force_charge_now : aucun changement (dry-run vérifié).
- Garde-fous intacts : hard floor 22 %, recovery urgent, latch anti-créneaux P2, P4/P5.
### Outillage (résout le blocage minimax « impossible de pousser dans n8n »)
- **`tools/n8n-pull.mjs <id> <fichier>`** : GET workflow → JSON local (clé API lue
automatiquement dans `~/.config/opencode/opencode.json`).
- **`tools/n8n-push.mjs <fichier> [id]`** : PUT REST (body minimal validé 04/08) avec
**backup serveur auto pré-push** (`modified/prepush-*.json`) et **`node --check`
sur chaque jsCode** (abandon si erreur de syntaxe).
- **`tools/patch-20260807-night-hc-grid.mjs`** : le patch P7 (remplacements exacts, rejoue-able).
- **`tools/dryrun-20260807-night-hc.mjs`** : harnais dry-run WINDOWS→BCL avec snapshot simulé.
- Cycle git : commit backup AVANT (`1debfe3`) → patch → push → commit APRÈS.
Rollback : `git checkout 1debfe3 -- workflows/evcs-surplus-controller-v2.6.json && node tools/n8n-push.mjs workflows/evcs-surplus-controller-v2.6.json`.
### À surveiller (prochaine nuit HC 01:24→07:24, VW branchée)
1. 01:24-01:25 : `source_mode` → `Réseau HC autorisé`, EDF ON, takeover helper ON ;
2. `start_grid_hc_min_current` → montée en courant (plafond EDF ON = helper 20 A / boost 28 A) ;
3. ACM en `evcs_takeover_hold` (batterie ~2 A, SOC stable) ;
4. 07:24 : fin de fenêtre → EDF OFF, retour profil jour.
---
*Document mis à jour le : 07/08/2026 (~15:50) — P7 charge HC nuit auto + tools n8n-pull/n8n-push*
## 12. Session du 07/08/2026 (soir) — R1 : Déduplication du moteur nuit
### Contexte
Refactor n°1 du plan de maintenabilité (conseil validé avec l'utilisateur) :
le moteur nuit complet (~161 lignes : nightSessionActive, batteryAssistAllowed,
nightGridTakeoverNeeded, nightPreferredSourceMode, cibles/triggers, latch...)
était calculé DEUX FOIS, dans WINDOWS_SOURCEV2 ET BATTERY_CLOUD_LOCKS.
Chaque patch nuit devait être appliqué 2× (P2, fix 04/08, P7) → risque de
divergence silencieuse entre sourceMode (WINDOWS) et conditions (BCL).
### Fix R1 (déployé 07/08 ~19:03, A/B validé)
- **WINDOWS_SOURCEV2 = unique calculateur.** Reprend les 4 éléments que seul BCL
calculait : `nightSessionStartMs`, `nightSessionNeedsCapture` version stricte
(critère startMs inclus), `batteryRecoveryAllowedNow`, `batteryTargetReached`.
Export `nightEvcsPreview` étendu (+ `nightTakeoverLatched`, `nightGridHcPreferred`).
- **BATTERY_CLOUD_LOCKS consomme** `prev.nightEvcsPreview` (bloc de 161 lignes
remplacé par ~50 lignes de lecture avec fallbacks sûrs : preview absent
→ session inactive / 'blocked'). −1,7K chars.
- Le shape de l'export `nightEvcs` de BCL (consommé par DECISION_ENGINEV2 et
HA_SERVICE_CALLS_OUTPUT) est strictement inchangé.
- **RÈGLE DÉSORMAIS : toute évolution de la logique nuit se fait UNIQUEMENT
dans WINDOWS_SOURCEV2.** BCL ne contient plus aucune logique nuit.
### Validation A/B (tools/ab-20260807-r1.mjs)
- Pipeline complet WINDOWS→BCL exécuté avant (git HEAD) / après (patché) sur les
7 scénarios golden + 2 cas limites (capture session history vide, clear résiduel).
- Sorties BCL **strictement identiques** (nightEvcs, conditions, safeguards, context).
- Fallback preview absent : pas de crash, retombe en 'blocked'.
- Piège rencontré : Date.now() dans le harnais → faux diffs de quelques ms ;
corrigé en construisant le snapshot une fois puis clone JSON par exécution.
### Outillage ajouté
- `tools/patch-20260807-r1-dedup-night.mjs` : le refactor (rejouable).
- `tools/ab-20260807-r1.mjs` : harnais A/B git HEAD vs working tree — réutilisable
pour tout futur refactor (modèle pour R3, la suite de tests golden).
### Prochaines étapes du plan validé
- R3 : suite de tests golden (~15 scénarios, exécutée avant chaque push).
- R2 : git source de vérité (src/nodes/*.js + assemblage dans n8n-push.mjs).
- Puis agent IA planificateur en mode ombre (jamais dans la boucle 30 s).
---
*Document mis à jour le : 07/08/2026 (~19:10) — R1 déployé, comportement inchangé (A/B), production stable*
## 13. Session du 07/08/2026 (soir) — R3 : Suite de tests golden
### Ce qui existe désormais (`tests/golden/`)
- **`run.mjs`** : exécute le VRAI code des nodes en chaîne complète
(INPUT_PARSE → WINDOWS_SOURCEV2 → BATTERY_CLOUD_LOCKS → DECISION_ENGINEV2
→ HA_SERVICE_CALLS_OUTPUT) dans une sandbox vm avec **horloge simulée**
(Date épinglée par scénario) sur un **snapshot HA réel** modifié par overrides.
- **`fixture-snapshot.json`** : snapshot production capturé depuis l'exécution
n8n 975833 (173 champs, re-capturable via `tools/fetch-snapshot-fixture.mjs`).
- **`scenarios.mjs`** : 16 scénarios avec attendus FIGÉS après revue manuelle :
6 nuit HC (P7 ON/OFF, sous-cible, hard floor, sans véhicule, EDF ON→start),
4 solaire (matin fort, matin nuageux, abondance P6, provocation P3),
garde SOC midi HC, force_grid, force_charge nuit, stop, overload, débranché.
### Usage
- `node tests/golden/run.mjs` → 16/16 ✅ ou liste des diffs + exit 1
- `--print` : affiche les résumés obtenus (pour autoriser une évolution)
- `--json` : sortie machine (utilisée pour figer les attendus)
- `--only=<motif>` : filtre par id de scénario
### Intégration au déploiement (le point clé)
`tools/n8n-push.mjs` exécute la suite AVANT tout PUT du workflow EVCS :
**golden rouge = rien n'est poussé**. Échappatoire consciente : `--skip-golden`.
Évolution volontaire du comportement → mettre à jour les attendus dans
`scenarios.mjs` DANS LE MÊME COMMIT que le patch.
### Validation par mutation
P7 volontairement cassé (`nightGridHcPreferred = false`) → la suite détecte
2 échecs avec diffs précis (sourceMode, takeover, EDF) et bloque le push ;
restauration → 16/16. Le filet fonctionne.
### Résumé testé par scénario
profile, sourceMode, state, decision, desiredCharging, currentA,
takeoverNeeded, action EDF ON, service helper takeover.
### Notes de comportement figées (découvertes en écrivant les attendus)
- SOC 20 la nuit avec maison sur batterie (−14 A) → `battery_current_hard_stop`
(bande lt25 : hard −8 A) + demande réseau simultanée. Protection cohérente.
- `force_charge_now` la nuit (toggle HC ON) → `start_manual_local_min_current`
ET takeover helper ON (session nuit active) : EDF peut venir en renfort. Voulu.
- Garde SOC>35 midi : sourceMode `grid_hc_allowed` mais `idle_wait_conditions`
(le grid HC midi ne s'active que SOC ≤ 35) — comportement épinglé par le test.
### Prochaines étapes du plan
- R2 : git source de vérité (src/nodes/*.js + assemblage dans n8n-push).
- Agent IA planificateur en mode ombre (après R2).
- Enrichir la suite au fil des incidents : chaque bug corrigé = +1 scénario.
---
*Document mis à jour le : 07/08/2026 (~19:20) — R3 en place, 16/16, push bloquant*