Files
EVCS/HANDOFF.md
Tallagrand Thierry b287e5ba73 Fix E: latch 'charge terminée' en session active + coupure recovery_done (incident rebond 08-09/08)
- DECISION: branche completed prioritaire dans la session active (avant VDF/source-lost)
  → état collant écrit au 1er cycle status=3, relais encore fermé
- WINDOWS: garde puissance <=300W sur Fix D (anti-glitch)
- Hystérésis: turn_off_recovery_done (bypass min_on, dry-run 5 cas)
- golden 23→25 (session armée status 3, flap statut post-latch)
- HANDOFF §18
2026-08-09 06:31:33 +02:00

52 KiB
Raw Blame History

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: " 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

// 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

// 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) :

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

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/<NODE>.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 <think>, 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