# 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//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/` → `modified/*_backup_.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/` (⚠️ 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: " AVANT le JSON => json.load ÉCHOUE. Extraction validée : grep -n '"name":"NOEUD"' 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//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: " 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 `** : GET workflow → JSON local (clé API lue automatiquement dans `~/.config/opencode/opencode.json`). - **`tools/n8n-push.mjs [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=` : 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* ## 14. Session du 07/08/2026 (soir) — P8 : Libération EDF quand le véhicule est plein ### Spécification utilisateur La nuit : charge dès 01:24 (P7) ✓, puis **quand la charge est finie → EDF OFF, sauf si la batterie solaire est sous le seuil** (alors EDF reste pour la recharger). ### Bugs découverts en traçant la spec 1. **Fix 04/08 logiquement inversé** : `(soc < 90 || vehicleChargeCompleted)` GARDAIT le latch takeover après la charge au lieu de le libérer. Jamais corrigé en réalité. 2. **P7 ne relâchait pas** : `nightGridHcPreferred` restait vrai tant que véhicule branché → l'EVCS redemandait EDF ON toutes les 30 s jusqu'à 07:24. 3. **Signal "voiture pleine" instable** : le VehicleDemandFilter retombe à false dès que le relais s'ouvre (session désarmée) → impossible de s'appuyer dessus seul. ### Solution : mémoire AUTO-ENTRETENUE via l'état surplus (P8) Chaîne (nouveau champ snapshot `prev_surplus_state` ← input_text.victron_evcs_surplus_state) : ``` charge → VDF détecte voiture pleine → state 'vehicle_full_or_paused' cycle N+1 : WINDOWS voit ce state → vehicleChargeCompleted=true → DECISION émet 'night_session_done_idle' cycles suivants : 'night_session_done_idle' s'auto-entretient fin : débranchement (state vehicle_not_connected) ou fin de fenêtre 07:24 ``` Effets quand vehicleChargeCompleted : - latch libéré (fix inversion 04/08 : `soc < 90 && !completed`) - préférence P7 relâchée ; sourceMode nuit → 'blocked' (zéro tentative de charge) - SOC > cible matin : takeover helper OFF → hystérésis coupe EDF (turn_off_recovered) - SOC ≤ cible : takeover helper ON → hystérésis maintient EDF (override) + ACM recharge la batterie à la cible → puis relâche. ⚠️ Le nom 'night_session_done_idle' ne doit matcher AUCUN pattern no-demand de l'hystérésis (full_or_paused / not_requesting_power / charge_complete / no_demand) — c'est ce qui permet à l'override batterie-basse de fonctionner. NE PAS RENOMMER sans vérifier. ### Nœuds modifiés (tools/patch-20260807-p8-release-full.mjs, rejouable) - Snapshot template : +prev_surplus_state - WINDOWS_SOURCEV2 : vehicleChargeCompleted collant, latch corrigé, P7 conditionné, preferred 'blocked' si plein, export preview - BATTERY_CLOUD_LOCKS : passthrough du flag dans nightEvcs - DECISION_ENGINEV2 : branche 'night_session_done_idle' (avant sourceMode blocked) ### Validation - 3 nouveaux scénarios golden (19 au total) : transition vehicle_full→done_idle (takeover turn_off), auto-entretien, batterie basse (takeover turn_on maintenu). - Les 16 scénarios existants inchangés (aucune régression). - Push via pipeline bloquant (golden 19/19), production stable. ### Comportement attendu cette nuit (VW branchée, checklist complète) 1. 01:24-01:25 : grid_hc_allowed → EDF ON → charge 6 A puis montée (P7) 2. VW pleine : vehicle_full_or_paused → night_session_done_idle → takeover OFF → EDF coupé par l'hystérésis ≤ 1 min après (si min_on 25 écoulé et SOC ≥ 30) 3. Batterie sous cible matin (35 été) : EDF maintenu + recharge ACM jusqu'à cible 4. 07:24 : fin de fenêtre → retour profil jour ## 15. Session du 07/08/2026 (soir) — R2 : Git = source de vérité du code des nodes ### Nouveau layout - **`src/nodes/`** = SOURCE DE VÉRITÉ : INPUT_PARSE.js, WINDOWS_SOURCEV2.js, BATTERY_CLOUD_LOCKS.js, DECISION_ENGINEV2.js, HA_SERVICE_CALLS_OUTPUT.js, BUILD_ACTIONS.js, HA_SNAPSHOT_TEMPLATE.json.tpl. - `workflows/evcs-surplus-controller-v2.6.json` = artefact déployable, reconstruit automatiquement depuis src/ à chaque push (src GAGNE toujours). - `tools/lib-node-map.mjs` (cartographie), `tools/n8n-extract.mjs` (JSON→src, uniquement pour resynchroniser après une édition UI n8n — à éviter), `tools/n8n-build.mjs [--check]` (src→JSON / vérif synchro). - `archive/` : toutes les copies historiques (decoupesurplus/, WINDOWS/, BATTERY/, DECISION/, HYSTERESIS/, EVCSV2.2.json, surplus.js...) — copies mortes, dérivées. - `.gitignore` : backup-workflow-*.json + modified/prepush-*.json (rollbacks locaux). ### WORKFLOW DE MODIFICATION DÉSORMAIS (règle inscrite dans AGENTS.md) ``` éditer src/nodes/.js node tools/n8n-push.mjs workflows/evcs-surplus-controller-v2.6.json → build src→JSON → node --check → GOLDEN 19/19 (bloquant) → backup serveur → PUT git commit (src + json + scénarios si évolution voulue) && git push ``` JAMAIS d'édition directe du JSON ou de l'UI n8n (écrasée au prochain push). ### Validations effectuées - Extract/build idempotents (extract → build → --check : zéro diff). - Test mutation : édition src → build injecte 1 node → restauration git+extract OK. - Pipeline complet de push exécuté en réel : build (no-op) → golden 19/19 → PUT OK. - Nota : tailles `ls` ≠ longueur de code (octets UTF-8 vs caractères, accents FR). ### Plan de maintenabilité : état - R1 ✅ déduplication moteur nuit (07/08) - R3 ✅ suite golden 19 scénarios bloquante (07/08) - R2 ✅ git source de vérité + build/push (07/08) - Suivant : agent IA planificateur en MODE OMBRE (jamais dans la boucle 30 s) — helpers advisor_*, validation bornée des sorties, kill-switch, cf. HANDOFF §13/§14. ## 16. Session du 07/08/2026 (nuit) — Agent IA planificateur en MODE OMBRE ### Ce qui tourne désormais Workflow n8n **"EVCS Advisor Shadow V1"** (id `tdfInZns7cqo8fU2`, actif, 15 min) : ``` Schedule 15min → HA Snapshot compact (template Jinja {{ {...} | tojson }}) → BUILD_PROMPT (kill-switch input_boolean.evcs_advisor_enabled, prompt FR ~4K chars) → IF_ENABLED → LLM_LOCAL_MODELS (GET 10.10.10.8:8000/v1/models, 5s) → LLM_LOCAL (modèle DYNAMIQUE = celui chargé sur la Spark, max_tokens 6000) → IF_LOCAL_OK → sinon LLM_CLOUD (OpenRouter anthropic/claude-haiku-4.5, credential chiffré "OpenRouter Advisor" id eOM9xofDHT0KMoeY — clé JAMAIS dans git) → VALIDATE_PARSE (strip , JSON strict, enums+bornes, invalide = REJET) → BUILD_HA_CALLS (helpers advisor_* + divergence + compteurs + logbook) → Execute HA Service ``` ### GARANTIE MODE OMBRE (structurelle, testée) BUILD_HA_CALLS n'écrit QUE vers `input_text/input_number.evcs_advisor_*` et logbook. Le test `tests/advisor/run.mjs` ÉCHOUE si une écriture sort de ce périmètre (16 tests : kill-switch, réponses valides/hors-bornes/non-JSON/ /offline, périmètre ombre, compteurs accord/divergence). Suite intégrée au push : `n8n-push.mjs workflows/evcs-advisor-shadow-v1.json`. ### Helpers HA (package NFS /mnt/ARCH3/docker/HA/packages/evcs_advisor_shadow.yaml) `evcs_advisor_enabled` (kill-switch) · `advisor_strategy` · `advisor_night_target_soc` (22-90) · `advisor_current_max_a` (6-28) · `advisor_ev_charge_tonight` · `advisor_reason` · `advisor_status` (ok/invalid/llm_offline) · `advisor_divergence` · `advisor_last_run` · `advisor_model` · `advisor_agree_count` / `advisor_diverge_count`. Carte dashboard « 🤖 Conseiller IA (ombre) » ajoutée à evcs_master.yaml (backup dans SAVE/). ### Première recommandation réelle (07/08 22:40, via qwen3-vl local) `auto_solar_plus` | cible 35% | charge VE cette nuit: oui | confiance 90% | « Prévision solaire forte (40.7 kWh), cible SOC matin basse adaptée. Véhicule chargé en HC avec réseau EDF. » → **accord avec le réel** (et avec P7/P8 !). ### Pièges rencontrés (pour mémoire) 1. Template /api/template DOIT être du Jinja : `{{ { ... } | tojson }}` — sans wrapper, HA renvoie le texte brut non rendu. 2. Modèle local = qwen3-vl **Thinking** : max_tokens 2500 → JSON tronqué par le raisonnement → rejeté proprement (`invalid: JSON non terminé`). Fix : 6000. (La validation stricte a fait exactement son travail dès le 1er run réel.) 3. MCP activate_workflow → erreur 415 : activer via REST POST .../activate body {}. 4. n8n-push : le fallback d'id par défaut (EVCS) était DANGEREUX avec un 2e workflow → supprimé, l'id vient du champ `id` du JSON ou de l'argument. ### Outillage multi-workflows (extension R2) `lib-node-map.mjs` : WORKFLOWS[] (evcs + advisor) · build/extract/push acceptent un fichier cible · `n8n-create.mjs` (POST création) · `gen-advisor-workflow.mjs` (squelette advisor rejouable, credentials/ids inclus). ### Note LLM local (analyse du 07/08, décision : statu quo surveillé) qwen3-vl-Thinking "fait le job" mais est sous-optimal pour l'advisor : variante Thinking (raisonnement non désactivable, 40-60 s/run, risque marginal de JSON tronqué — rejeté proprement par la validation). Le bon modèle local est qwen3.6-35b quand il est chargé (use-code). **Option prête si les compteurs se dégradent** (décision utilisateur : ne rien changer pour l'instant) : routage intelligent = 1 IF après LLM_LOCAL_MODELS (local seulement si l'id du modèle chargé n'est pas vl/thinking, sinon cloud direct) + éventuellement cloud moins cher (gemini-2.5-flash-lite ~1€/mois vs haiku-4.5 ~9€/mois plein temps). Implémentation ~30 min, tests advisor inchangés. ### Phase 3 — critères de bascule (dans 2-3 semaines d'observation) 1. Taux d'accord stratégie ≥ 80% (compteurs agree/diverge sur la carte) ; 2. Zéro sortie invalide NON rejetée (advisor_status) ; 3. Divergences analysées = soit l'agent a raison (améliorer les règles), soit il a tort (améliorer le prompt) — les deux font progresser le système. → Alors premier câblage réel LIMITÉ : night_target_soc uniquement, derrière un nouveau toggle `advisor_apply_night_target`, le reste inchangé. ## 17. Session du 08/08/2026 — Incident rebond EDF nuit 07-08/08 : fixes A/B/C/D ### Incident (captures HA analysées, faits dans docs/vision-notes.md) P7 a chargé la VW dès 01:24 ✓ (session 1h32m). Ensuite : 1. Fin de charge détectée très tardivement (~04:15 au lieu de ~02:56) ; 2. Rebond EDF ON/OFF période ~10 min autour de SOC=35 (= cible matin) + flapping ACM Charging/passthru 2-4 s (03:58→04:16) ; 3. Débranchement VW (04:17) → takeover bien relâché mais EDF maintenu (hold_on_min_runtime 25 min) → arrêt manuel utilisateur 04:25 (stopped_user). ### Root causes - **A — point dégénéré SOC==cible** (WINDOWS_SOURCEV2) : engage takeover `soc <= cible`, release `soc > cible`, recovery ACM `soc < cible` (strict), et l'ACM recharge PILE à la cible → aucune position stable, oscillation. - **B — snapshot incomplet** : `evcs_night_session_start_soc/_at` jamais relus (absents du template) → `nightSessionNeedsCapture` vrai à CHAQUE cycle → "Night Session Start At" réécrit toutes les 30 s toute la nuit (visible logs), budget décharge nuit faussé (start SOC glissant). - **C — hystérésis grid** : pas de coupure anticipée au débranchement (min_on 25 min s'applique même véhicule parti et SOC ≥ seuil OFF). - **D — détection fin de charge fragile** : VDF armé uniquement session pleinement active ; stop par garde (ex battery_current_hard_stop) → filtre désarmé → 'vehicle_full_or_paused' inatteignable pendant longtemps. ### Fixes déployés (production 08/08 ~14:00, golden 23/23) - **Fix A** (WINDOWS_SOURCEV2 + BCL passthrough + DECISION) : nouvel état auto-entretenu **`night_recovery_done_idle`** émis quand vehicleChargeCompleted && SOC ≥ cible. Takeover relâché DÉFINITIVEMENT ; ré-engagement uniquement si SOC ≤ cible − 2 (bande NIGHT_RECOVERY_REARM_BAND_PCT). ⚠ Le nom ne matche aucun pattern no-demand de l'hystérésis (comme done_idle). Structure takeover : completed ? !nightRecoveryDone : (trigger || urgent || latch). - **Fix B** (HA_SNAPSHOT_TEMPLATE) : +`evcs_night_session_start_soc`, +`evcs_night_session_start_at` (helpers input_text existants, vérifiés via MCP). - **Fix C** (workflow hystérésis 7iIlu7LLTW7TZA8o, patch tools/patch-20260808-c-unplug-cut.mjs rejouable) : nouvel état **`turn_off_vehicle_unplugged`** = véhicule débranché (es=vehicle_not_connected) + latch ON + grid ON + pas d'override + SOC ≥ seuil OFF → coupure immédiate (bypass min_on) + notification. Dry-run 4 cas validé (incident/SOC bas/ takeover ON/recovery_done) AVANT push. Le fichier local du workflow est désormais suivi : workflows/victron-grid-hysteresis-v1.json. - **Fix D** (WINDOWS_SOURCEV2) : `evcs_status_raw == 3` (Victron "charged") vaut fin de charge immédiate, sans attendre le VDF. ### Golden : 19 → 23 scénarios - 2 attendus mis à jour (transition/auto-entretien : SOC ≥ cible → état `night_recovery_done_idle` désormais) ; - 4 nouveaux : `night-recovery-atteinte-soc-cible` (LE cas du rebond), `night-recovery-done-bande-collante` (34 % dans la bande), `night-recovery-rearm-sous-bande` (32 % → ré-engagement), `night-vw-statut-charged` (Fix D). - Piège outillage : `tests/golden/run.mjs` exécute le JSON BUILDÉ (workflows/*.json), pas src/ → toujours `node tools/n8n-build.mjs` avant un run golden manuel (n8n-push le fait automatiquement). ### Documentation - MANUEL_COMPLET.md §11.6 : GRAFCET complet du cycle nuit (G1 EVCS / G2 hystérésis) + états surplus fin de nuit + résumé incident. - docs/vision-notes.md : OCR/faits des 9 captures de l'incident. ### À surveiller (prochaine nuit HC, VW branchée) 1. Fin de charge : passage à vehicle_full_or_paused OU night_recovery_done_idle rapide (Fix D si la VW publie le statut 3) ; 2. SOC à la cible → UNE coupure EDF, puis plus aucun ON/OFF (sauf SOC ≤ cible−2) ; 3. "Night Session Start At" écrit UNE fois en début de session (Fix B) ; 4. Débranchement → EDF OFF ≤ 1 min (turn_off_vehicle_unplugged, Fix C). --- *Document mis à jour le : 08/08/2026 (~14:15) — fixes A/B/C/D déployés, golden 23/23* ## 18. Session du 09/08/2026 — Incident nuit 08-09/08 : le rebond est revenu par un AUTRE chemin (Fix E) ### Incident (captures HA, VW branchée, HC, VW pleine vers ~03:00) Fix A/D du 08/08 ont bien fonctionné CÔTÉ WINDOWS (status 3 → completed → takeover Désactivé, source blocked). MAIS l'état EVCS observé était `hold_min_run_source_lost` / `stop_source_lost` — jamais `night_recovery_done_idle`. Cycles ~30 min de 03:00 à 05:40 : EDF ON (2531 W réseau, SOC 51 % > cible 35) → arrêt urgence utilisateur 05:43 (EDF URGENCE OFF + stratégie stop) → débranchement. ### Root cause (ordre des branches DECISION) 1. Status 3 arrive PENDANT la session armée (relais fermé, chg=1/perm=1/l=1). 2. WINDOWS → sourceMode 'blocked' (Fix D) ; mais dans DECISION la branche session ACTIVE traite 'blocked' comme « source perdue » : `hold_min_run_source_lost` (25 min de session zombie !) puis `stop_source_lost`. La branche P8 (état collant) est dans la chaîne NON-active → jamais atteinte. 3. Au stop, le relais s'ouvre → le statut EVCS RETOMBE (3 → 1/6, même piège que le VDF documenté en P8) → completed=false → re-P7 → takeover ON 1 cycle → `edf_general_ok_on` rallume EDF + restart → status 3 → goto 2. Rebond ~30 min. 4. L'hystérésis adoptait l'EDF ON du cycle de flap puis `hold_on_min_runtime` 25 min (ov=0/eg=0 ensuite, mais latch pris) → EDF ON quasi permanent. ### Fix E (déployé 09/08 ~06:25, golden 25/25) - **DECISION** : nouvelle PREMIÈRE branche du bloc session-active (avant VDF) : `night_hc && vehicleChargeCompleted && !force*` → setOff immédiat avec l'état collant (`night_recovery_done_idle` si SOC ≥ cible, sinon `night_session_done_idle`), decision `stop_vehicle_full_or_paused`. → l'état collant est écrit AU PREMIER cycle où status=3, relais encore fermé ; les retombées de statut ensuite sont sans effet (prev state dans la liste). - **WINDOWS** : garde puissance sur Fix D (`status==3 && evcsAcPowerW <= 300 W`) pour ne jamais latcher sur un glitch en pleine charge. - **Hystérésis** (patch tools/patch-20260809-e2-recovery-done-cut.mjs) : `turn_off_recovery_done` = es=night_recovery_done_idle + pas d'override + SOC ≥ seuil OFF → coupure EDF immédiate (bypass min_on 25). Sûr car l'état est collant (le takeover ne peut pas revenir, sauf SOC ≤ cible−2 → override). Dry-run 5 cas validé avant push. ### Golden : 23 → 25 - `night-charged-en-session-active` : status 3 en session ARMÉE → arrêt propre + latch immédiat (LE chemin de l'incident) ; - `night-flap-statut-apres-latch` : relais ouvert, statut retombé à 1, prev=night_recovery_done_idle → collant, aucun re-P7/EDF. ### Comportement attendu au REBRANCHEMENT d'une VW pleine en HC 1. Session nuit → P7 → EDF ON → tentative de charge (inévitable : le système ne peut pas savoir que la VW est pleine sans fermer le relais) ; 2. VW refuse (0 W, status 3) → ≤ 1 min : `night_recovery_done_idle` collant, takeover OFF, session stoppée proprement ; 3. Hystérésis : `turn_off_recovery_done` → EDF OFF ≤ 1 min (plus de min_on 25) ; 4. Plus RIEN jusqu'au débranchement / fin de fenêtre (flaps de statut ignorés). Total attendu : EDF ON ~2-3 min UNE seule fois, puis silence. --- *Document mis à jour le : 09/08/2026 (~06:35) — Fix E déployé (EVCS + hystérésis), golden 25/25* ## 19. Session du 09/08/2026 (suite) — Fix F : la mémoire "charge terminée" est prioritaire sur les gardes ### Test de rebranchement utilisateur (~06:33, VW rebranchée en HC) 1. ✅ Fix E validé en réel : 06:51:00 `night_recovery_done_idle` latché, 06:51:03 `turn_off_recovery_done` → EDF coupé immédiatement (bypass min_on). 2. ❌ 06:53-06:56 : la VW a relancé une charge (status 2) qui, EDF OFF, tirait sur la batterie (−47 A) → garde `mustStopHard` → état `battery_current_hard_stop` ÉCRIT PAR-DESSUS le collant dans le helper → completed perdu → re-P7 → EDF rallumé (hold_on_evcs_override, re-min_on 25). → Le collant ne survivait à AUCUN état émis par une branche plus prioritaire. ### Fix F (déployé 09/08 ~07:00, golden 26/26) - DECISION : nouvelle branche dans la cascade PRINCIPALE, entre `vehicle_not_connected` et `mustStopHard` : `night_hc && vehicleChargeCompleted && !force*` → setOff avec l'état collant (recovery_done/done_idle), `mustStop` si la borne est encore armée. → l'état collant est réécrit à CHAQUE cycle tant que VW branchée + fenêtre, il survit aux micro-charges/gardes. Aucune perte de protection (le setOff coupe chg/perm/latch comme un hard stop). - La branche Fix E interne (session active) supprimée : remplacée par la branche haute. La branche P8 historique (chaîne non-active) reste en filet. - Golden 26 : `night-hard-stop-necrase-pas-done` (micro-charge −60 A sur batterie avec prev=recovery_done → l'état reste recovery_done, stop ferme). ### Leçon d'architecture (pour le futur) La "mémoire" portée par input_text.victron_evcs_surplus_state est fragile : tout état émis l'écrase. Fix F la protège en réécrivant chaque cycle, mais la solution PROPRE à terme est un helper dédié (input_boolean victron_evcs_night_charge_done, package YAML + reload HA) si un nouveau cas d'écrasement apparaît. ### Relecture de la nuit 08-09/08 (après coup) La VW n'a pris que 5 kWh cette nuit, morcelés par les stops zombies (§18) : elle n'était probablement PAS pleine. Les "micro-charges" du matin étaient de vraies reprises. À 07:00 (post-Fix F) : charging_step_up_grid_hc = charge HC nominale jusqu'à la fin de fenêtre 07:24. Le collant ne se posera que sur un VRAI signal de fin (VDF no-demand ou status 3) — et tiendra désormais. --- *Document mis à jour le : 09/08/2026 (~07:05) — Fix F déployé, golden 26/26* ## 20. Session du 09/08/2026 — PREMIER CÂBLAGE RÉEL de l'advisor : cible SOC matin ### Décision utilisateur Météo Bretagne au beau fixe (47-58 kWh/j prévus) = conditions idéales pour tester le câblage avec risque minimal. Périmètre STRICTEMENT limité au plan §16 : `night_target_soc` uniquement, derrière un toggle, rollback 1 clic. ### Ce qui est câblé `batteryMorningTargetPct` (WINDOWS_SOURCEV2) suit `input_number.evcs_advisor_night_target_soc` SI ET SEULEMENT SI : 1. `input_boolean.evcs_advisor_apply_night_target` = ON (nouveau toggle, initial OFF, package evcs_advisor_shadow.yaml + carte dashboard) ; 2. `input_text.evcs_advisor_status` = 'ok' (exact) ; 3. recommandation fraîche ≤ 120 min (parse `DD/MM HH:mm` de advisor_last_run, année courante, garde passage d'année — parseAdvisorRunToMs INPUT_PARSE) ; 4. valeur clampée [max(22, hard floor), 90]. Sinon → règles forecast historiques (`batteryMorningTargetRulePct`), aucun changement de comportement. Le toggle OFF = état d'origine. ### Chaîne technique - Snapshot : +advisor_apply_night_target, +advisor_night_target_soc, +advisor_status, +advisor_last_run (states() d'une entité absente → 'unknown' → toggle vu OFF → sûr AVANT le reload HA). - INPUT_PARSE : params.advisorApplyNightTarget / advisorNightTargetSocPct / advisorStatusOk / advisorLastRunMs. - WINDOWS : override borné + export advisorNightTargetApplied + batteryMorningTargetRulePct (preview) ; BCL passthrough ; DECISION : la reason affiche `target=X% (ADVISOR)` quand appliqué → visible logbook/carte. - Impact aval automatique : gridTakeoverTriggerPct, grid_forced vs hc_allowed, recovery post-charge, bande anti-rebond — tous dérivés de la cible. ### Golden 26 → 29 - `night-advisor-target-applique` : SOC 30, advisor 22 ON/ok/frais → grid_hc_allowed (au lieu de grid_forced par la règle 35) ; - `night-advisor-target-stale` : > 120 min → fallback règles (grid_forced) ; - `night-advisor-toggle-off` : toggle OFF → règles inchangées. ### Sécurités NON modifiées Hard floor 22 %, hysteresis grid ON ≤ 20 %, recovery urgent départ, latch anti-créneaux, collant fin de charge (Fix A/E2/F). L'advisor ne peut RIEN piloter d'autre (VALIDATE_PARSE borne night_target_soc 22-90 côté advisor, clamp identique côté contrôleur). ### Rollback (3 niveaux) 1. Toggle « APPLIQUER cible SOC matin » OFF → effet au cycle suivant (30 s) ; 2. Kill-switch advisor OFF → plus de runs → fraîcheur > 120 min → fallback auto en ≤ 2 h même si le toggle reste ON ; 3. `git revert` du commit + `node tools/n8n-push.mjs workflows/evcs-surplus-controller-v2.6.json`. ### Surveillance (2-3 nuits) - Carte Conseiller IA : « Cible SOC matin (agent) » vs « (réel) » doivent converger quand le toggle est ON ; - Logbook « Victron EVCS - Reason » la nuit : `target=X% (ADVISOR)` ; - Le matin : SOC batterie au lever du soleil ≈ cible advisor, remontée solaire OK ; - Si SOC matin < 22 % ou EDF nuit anormal → toggle OFF + capture + analyse. ### Action utilisateur requise après ce déploiement Recharger HA pour créer le toggle : Outils développeur → YAML → « ENTRÉES BOOLÉENNES (input_boolean) » Recharger (ou redémarrage HA), puis activer le toggle sur la carte Conseiller IA (dashboard EVCS Master rechargé). --- *Document mis à jour le : 09/08/2026 (~07:35) — câblage advisor night_target déployé (toggle OFF par défaut), golden 29/29* ## 21. Session du 09/08/2026 — P9 : charge réseau avant départ + sélecteur véhicule VW/JEEP ### Contexte clarifié par l'utilisateur (IMPORTANT pour comprendre les incidents) DEUX véhicules alternent sur la borne : - **VW** (hybride) : départ ~08:50, ne publie PAS franchement "charged" (statut 3 rare/fugace) → détection fin de charge = VDF surtout ; - **JEEP** : départ ~06:30, publie "charged" franchement. Relecture des incidents : nuit 08-09/08 = JEEP (rebonds §18) ; matin 09/08 = VW déjà pleine (chargée la veille en solaire) rebranchée à la place de la JEEP. L'heure de départ unique (08:50) était un réglage VW ; la JEEP part à 06:30 (dans la fenêtre HC → couverte par la mécanique nuit existante). ### Nouveaux helpers (package /mnt/ARCH3/docker/HA/packages/victron_evcs_vehicles.yaml) - `input_select.victron_evcs_vehicle` (VW/JEEP, défaut VW) — à sélectionner au branchement. NB : l'ancien `input_select.victron_evcs_target_vehicle` (None/Jeep/Zero, créé UI) n'était câblé nulle part → retiré de la carte, remplacé par le nouveau. À supprimer un jour via l'UI. - `input_datetime.victron_evcs_departure_time_jeep` (06:30). L'existant `victron_evcs_departure_time` (08:50) devient l'heure VW. - `input_boolean.victron_evcs_allow_grid_departure_urgent` (toggle P9, OFF). Carte "Paramètres utilisateur" (evcs_master.yaml) mise à jour. ⚠ RELOAD HA requis (input_boolean + input_datetime + input_select). ### P9 (golden 33/33, déployé 09/08 ~08:27) - INPUT_PARSE : `departureTimeMin` suit le véhicule sélectionné (JEEP → departure_time_jeep, fallback VW) ; impacte aussi departurePriorityActive et batteryRecoveryUrgent nuit (donc la JEEP à 06:30 est mieux servie). - WINDOWS : fenêtre `departureWindowActive` = toggle ON + véhicule branché + HORS fenêtre HC + départ dans ≤ 120 min. Si charge non terminée → `departureUrgentActive` → sourceMode `grid_hc_allowed` (prend le pas sur blocked/solar_potential/hybrid_probe, JAMAIS sur solar_only ni stop/force*). Fin de charge (VDF vehicle_full_or_paused relu, ou statut 3) → `departureChargeDone` → sourceMode blocked + état collant. - BCL : `gridAllowedBySoc` inclut departureUrgentActive (sinon SOC>35 bloquait). - DECISION : branche collante `departure_done_idle` (même principe que Fix F, au-dessus des gardes, mustStop si borne armée). - HA_SERVICE_CALLS : helper takeover tenu ON pendant departureUrgentActive (override hystérésis + maintien batterie ACM pendant la charge pré-départ). - Hystérésis (patch tools/patch-20260809-p9-departure-cut.mjs) : coupure immédiate E2 étendue à `departure_done_idle`. ### Golden 29 → 33 p9-depart-vw-grid (fenêtre active, SOC 45 > 35 → réseau quand même) · p9-depart-jeep-passe (sélecteur : départ JEEP passé → rien) · p9-done-idle-collant · p9-toggle-off (comportement historique). ### Mode d'emploi utilisateur 1. Recharger HA (YAML : input_boolean, input_datetime, input_select) ; 2. Sur la carte : sélectionner le véhicule au branchement (VW ou JEEP) ; 3. Activer « ⚡ Réseau avant départ (P9) » ; 4. La JEEP profite de sa vraie heure 06:30 dès cette nuit (recovery urgent nuit) ; la VW aura sa fenêtre 06:50-08:50 hors HC si sous-chargée. Rollback : toggle P9 OFF (30 s) ; sélecteur sans effet si P9 OFF (sauf heure de départ pour la priorité/urgence nuit — voulu). --- *Document mis à jour le : 09/08/2026 (~08:35) — P9 + sélecteur véhicule déployés, golden 33/33* ## 22. Session du 09/08/2026 — P10 : mémoire "véhicule plein" en journée ### Incident (captures utilisateur 09:49-09:57) VW pleine rebranchée ~07:00. À 09:49, PV fort (5,5 kW) → le surplus solaire lance une tentative (morning_solar, solar_only) : 6 A commandés, 0 W pris, VRM affiche "Charged". Sans mémoire jour, le cycle se répète toutes les ~15 min TOUTE la journée (VDF stop → block → re-start). Zéro coût énergie (solaire, EDF OFF) mais usure relais borne/VW + bruit. Les états collants P8/P9 étaient scopés nuit / fenêtre départ. ### Fix P10 (déployé 09/08 10:04, golden 36/36, validé live à 10:04:30) - WINDOWS : `dayVehicleFullIdle` collant = véhicule branché + hors fenêtre HC + (prev ∈ {day_vehicle_full_idle, vehicle_full_or_paused, night_recovery_done_idle, night_session_done_idle, departure_done_idle} OU statut 3 ≤ 300 W). sourceMode → 'blocked' (y compris face à solar_only : charger un véhicule plein ne sert à rien). - HÉRITAGES bidirectionnels : le jour hérite des états done de la nuit (07:24 → aucune re-tentative matinale) ; la nuit hérite du jour (vehicleChargeCompleted inclut day_vehicle_full_idle → pas de tentative 01:24 si resté branché plein). - DECISION : branche collante 'day_vehicle_full_idle' au-dessus des gardes (principe Fix F), stop propre si session armée. - Libération : DÉBRANCHEMENT (vehicle_not_connected) — ou échappatoires force_charge_now / force_grid_now (testé golden). - Hystérésis : nom sans pattern no-demand ✓, aucun changement nécessaire (EDF pas impliqué dans ce chemin). ### Golden 33 → 36 day-vw-pleine-statut3 (session armée 0 W statut 3 → collant) · day-full-heritage-nuit (prev=night_recovery_done_idle → pas de solaire) · day-full-echappatoire-force-charge (force_charge_now relance). ### Validation live 10:04:14 PUT → 10:04:30 : `day_vehicle_full_idle` latché sur la VW pleine en cours de tentative ; statut retombé à 6 (relais ouvert) sans relance. ### Cycle de vie complet d'un véhicule branché (résumé) ``` branché → tentatives légitimes (nuit P7 / jour solaire / P9 départ) → plein détecté (statut 3 ou VDF) → collant : night_*_done_idle / departure_done_idle / day_vehicle_full_idle (héritages entre périodes, réécrit chaque cycle au-dessus des gardes) → DÉBRANCHEMENT = seule remise à zéro (+ force_* en échappatoire) ``` --- *Document mis à jour le : 09/08/2026 (~10:10) — P10 déployé et validé live, golden 36/36* ## 23. Session du 12/08/2026 — P11 : reset du collant au changement de véhicule ### Incident (nuit 12/08, captures utilisateur) JEEP branchée 01:24, chargée en HC (cible 35% ADVISOR), fin de charge franche → état collant `night_recovery_done_idle`. Utilisateur débranche la JEEP et branche la **VW** à sa place. L'état collant a SURVÉCU (le collant n'était remis à zéro que par `vehicle_not_connected`, non vu de façon franche lors du swap) → la VW était bloquée « charge véhicule terminée » alors que c'était celle de la JEEP. La VW (déchargée) ne chargeait pas. ### Cause de conception Tous les états collants "plein" (P8 night_*, P9 departure_*, P10 day_*) se libéraient UNIQUEMENT au débranchement (vehicle_not_connected). Un remplacement de véhicule rapide (ou un statut EVCS qui ne repasse pas franchement "déconnecté") laissait le collant en place → nouveau véhicule bloqué. ### Fix P11 (déployé 12/08 05:00, golden 39/39, validé live à 05:00:30) Signal fiable : `sensor.victron_evcs_charging_time` repart de 0 à CHAQUE nouvelle connexion physique. - Snapshot : +`evcs_charging_time` ; INPUT_PARSE : `parseChargingTimeToSec` ("1h 49m 53s"/"8m 27s"/"0s" → secondes), `parsed.evcsChargingTimeSec`. - WINDOWS : `sessionFresh` = véhicule branché ET charging_time ≤ 90 s. Neutralise `vehicleChargeCompleted`, `dayVehicleFullIdle`, `departureChargeDone` ET `evcsStatusCharged` (statut 3 résiduel ignoré le temps que la borne renégocie) → le système RETESTE le véhicule neuf. - Export `sessionFresh` (traçabilité). ### Golden 36 → 39 p11-changement-vehicule-nuit (prev=night_recovery_done_idle + charging_time 15s → night_wait_grid_takeover, retest) · p11-changement-vehicule-jour (day_vehicle_full_idle + 20s → solaire repart) · p11-session-etablie-reste-collant (charging_time 2h05 → reste collant, pas de reset abusif). ### Validation live 05:00:00 PUT → 05:00:30 : la VW est passée de `night_recovery_done_idle` (bloqué) à `night_wait_grid_takeover` / source « Réseau HC autorisé ». ### Récapitulatif des remises à zéro du collant "plein" 1. DÉBRANCHEMENT (vehicle_not_connected) ; 2. CHANGEMENT DE VÉHICULE / nouvelle connexion (charging_time ≤ 90 s, P11) ; 3. force_charge_now / force_grid_now (échappatoires manuelles). ## Diagnostic en attente de vérification (Qwen, 22/08) Voici mon analyse et plan de correction : Diagnostic Le bug est dans HA_SERVICE_CALLS_OUTPUT.js:987-996 : edf_general_ok_on: { execute: ['grid_forced', 'grid_hc_allowed'].includes(context.sourceMode) && !parsed.gridAvailable, ... } Il n'existe pas de edf_general_ok_off. Le workflow ne coupe JAMAIS activement EDF. Il ne fait que le rallumer quand sourceMode est grid ET que le réseau n'est pas encore disponible. Quand sourceMode revient à solar_only/blocked, le switch reste ON dans l'état qu'il avait. Conséquence : après la fenêtre HC nocturne (~06:00), EDF reste allumé. La présence du réseau bloque la production PV (Victron plafonne à ~100 W). Le débranchement du véhicule résout parce que c'est le seul mécanisme qui coupe physiquement le switch. Plan de correction Fichier : src/nodes/HA_SERVICE_CALLS_OUTPUT.js Ajouter un call edf_general_ok_off juste après edf_general_ok_on (ligne 996) : edf_general_ok_off: { execute: !['grid_forced', 'grid_hc_allowed'].includes(context.sourceMode) && parsed.gridAvailable &parsed.evcsVehicle **Verdict (Claude, 22/08) : diagnostic ÉCARTÉ, voir §24 ci-dessous.** Le symptôme est correctement identifié (EDF ne se coupe jamais activement après la fenêtre HC) mais la cause et le correctif proposés sont faux : le bug n'est pas dans `HA_SERVICE_CALLS_OUTPUT.js` (qui n'a jamais eu vocation à couper EDF — la coupure est le rôle exclusif du workflow séparé "Victron - Battery / Grid Hysteresis Fix V1"), et ajouter un `edf_general_ok_off` côté EVCS créerait un deuxième écrivain non coordonné sur `switch.edf_general_ok_switch` (courses 30s/1min entre les deux workflows, le hysteresis reprenant la main au cycle suivant via son propre `latchIsOn` et rallumant probablement EDF quand même). Le vrai bug et son correctif (Fix G, côté hystérésis) sont documentés en §24. --- *Document mis à jour le : 22/08/2026 (~18:00) — Fix G déployé (hystérésis), diagnostic Qwen écarté, golden-hysteresis 7/7* ## 24. Session du 22/08/2026 — Fix G : coupure EDF quand l'override EVCS disparaît mi-session (hystérésis) ### Incident (nuit 21-22/08) Le takeover nuit (P7) a correctement rechargé la batterie à ~35 % via EDF. Quand la fenêtre HC s'est fermée (~06:00-07:24) avec le véhicule **encore branché en session active** (pas encore flag "charge terminée"), EDF est resté allumé — le réseau a plafonné le MPPT Victron à ~100 W, privant la maison de 600-4000 W de solaire disponible (elle tirait ~1000 W d'EDF). Seul le débranchement du véhicule a libéré EDF (2179 W de PV immédiatement après). ### Root cause Côté EVCS (`WINDOWS_SOURCEV2.js`/`DECISION_ENGINEV2.js`) : **pas le bug**. Chaque cycle, `nightSessionActive` et tout le moteur nuit sont activement recalculés à `false`/`'blocked'` dès la fermeture de fenêtre — `helper night_grid_takeover_needed` passe à `off` et `source_mode` à `'Bloqué'` correctement et immédiatement, même mi-session. MAIS les deux branches "collantes" de fin de session (Fix F, exige `profile === 'night_hc'` ; P10 `day_vehicle_full_idle`, exige que le `surplus_state` précédent soit déjà un pattern "terminé") ne se déclenchent PAS sur le cycle exact où la fenêtre se ferme mi-session non-terminée. Le système retombe sur des états génériques transitoires (`stop_source_lost` puis `idle_wait_source`). Côté **Grid Hysteresis** (`workflows/victron-grid-hysteresis-v1.json`, node "Compute Grid Hysteresis") : **c'est là le bug**. Les règles de coupure immédiate existantes (Fix C `turn_off_vehicle_unplugged`, Fix E2 `turn_off_recovery_done`) ne matchent que des noms exacts de `surplus_state` (`vehicle_not_connected`, `night_recovery_done_idle`, `departure_done_idle`) — aucune ne matche `stop_source_lost`/`idle_wait_source` (ni `day_vehicle_full_idle` lui-même). Sans matche, le code retombe sur la boucle générique qui maintient EDF jusqu'au seuil statique `grid_soc_off` (35 %, `input_number.victron_grid_soc_off`), totalement découplé de la cible dynamique de l'EVCS — potentiellement des heures, d'autant qu'EDF-ON lui-même bride la recharge solaire organique. ### Fix G (déployé 22/08 ~18:00, golden-hysteresis 7/7) Au lieu d'ajouter encore un nom d'état à une liste blanche fragile, nouvelle règle **générique** dans "Compute Grid Hysteresis", basée sur `evcsGridOverride` (déjà fiable) plutôt que sur le nom de `surplus_state` — couvre donc aussi automatiquement tout futur nom d'état non énuméré : ```javascript if ( !forcedCut && latchIsOn && gridIsOn && !evcsGridOverride && soc > gridSocOn && // 22%, PAS gridSocOff 35% — protège le cas // indépendant "SOC bas" qui doit tenir // jusqu'à gridSocOff evcsSourceModeLooksValid // garde anti-glitch (lecture HA vide/ // unavailable/unknown → pas d'action) ) { desiredGrid = 'off'; desiredLatch = 'off'; state = 'turn_off_evcs_override_released'; forcedCut = true; } ``` Coupure immédiate, bypass min_on (25 min), comme Fix C/E2. Décision validée avec l'utilisateur : vitesse de libération prioritaire sur le risque résiduel (un glitch de lecture HA sur un seul cycle pourrait causer une coupure/ rallumage isolé, auto-diagnosticable dans le logbook — pas de chatter soutenu car il faudrait une lecture fausse à *chaque* cycle). ### Nouvelle suite de tests : `tests/golden-hysteresis/` (7 scénarios, première suite pour ce workflow — 0 → 7) `g-incident-gap-mid-session`/`g-incident-gap-idle-wait-source` (les deux noms d'état zombies de l'incident, min_on non écoulé) · `g-incident-replay-hc-boundary` (rejeu complet, min_on déjà écoulé aussi — prouve que ce n'est pas seulement un bypass min_on) · `g-baseline-low-soc-no-override` (garde non-régression : SOC bas indépendant doit continuer à tenir) · `g-regression-fixc-unplugged` / `g-regression-fixe2-recovery-done` (Fix C/E2 toujours prioritaires, pas absorbés par Fix G) · `g-glitch-guard-unavailable` (garde anti-glitch). Lancée AVANT patch : 3 échecs (les scénarios d'incident, stuck sur `hold_on_min_runtime`/`hold_on_until_soc_off`) + 4 succès (gardes déjà bonnes) — confirme que le harnais détecte vraiment le bug. Après patch : 7/7. Câblée dans `tools/n8n-push.mjs` (nouveau `isHysteresisWorkflow`), bloquante au push comme les 2 autres workflows. ### Outillage `tools/patch-20260822-g-override-released-cut.mjs` (rejouable, même patron que `patch-20260809-e2-recovery-done-cut.mjs`) · `tests/golden-hysteresis/{fixture-snapshot.json,run.mjs,scenarios.mjs}`. ### Déploiement `node tools/n8n-push.mjs workflows/victron-grid-hysteresis-v1.json` → backup serveur auto (`modified/prepush-7iIlu7LLTW7TZA8o-*.json`) → syntaxe OK → golden-hysteresis 7/7 → PUT OK. `grid_min_on_minutes` réel vérifié via MCP `ha-get-state` avant de figer le fixture : **25** (pas le fallback 45 du code). ### Hors périmètre (volontairement) Aucun changement côté `src/nodes/*` (EVCS V2.6) — le fix est entièrement côté hystérésis. Amélioration cosmétique future possible, non incluse ici : remplacer les noms zombies `stop_source_lost`/`idle_wait_source` par un état de transition dédié pour la lisibilité dashboard/logbook. ### À surveiller (prochaine fermeture de fenêtre HC avec véhicule encore branché mi-session) 1. Logbook doit montrer `turn_off_evcs_override_released` peu après la fermeture de fenêtre si le véhicule n'est pas encore flag "terminé" ; 2. La PV doit remonter (MPPT non-plafonné) immédiatement après ; 3. Aucune coupure/rallumage répété (`turn_off_evcs_override_released` suivi de près par `turn_on_evcs_override` répétitivement) — signe d'un glitch de lecture HA à investiguer si observé. --- *Document mis à jour le : 22/08/2026 (~18:05) — Fix G déployé et validé (syntaxe + golden-hysteresis 7/7 + PUT OK), à confirmer en conditions réelles à la prochaine fermeture de fenêtre HC mi-session