Files
EVCS/HANDOFF.md
Tallagrand Thierry f18c6e07f6 Nuit : la recovery batterie est une passe unique, pas un oscillateur
La bande de 2 points du Fix A engageait le takeover a SOC 33 et le relachait
a 35 : 0,34 kWh, soit une demi-heure de conso nocturne plate. Trois cycles EDF
la nuit du 26-27/08, six la nuit du 20-21/08, 2,5 kWh importes pour rien.

Une seule passe de recovery par session de nuit. Le plancher dur reste un
terme independant, l hysteresis garde sa bande 20/30, et le vrai critere de
re-engagement (un budget d energie) appartient au bilan, pas a une constante
de plus ici.

Golden 40 -> 42, preuve par mutation. Documente aussi deux acteurs non lus :
les charges programmees de l ESS (45 % la nuit, contre notre cible 35 %) et
le regime majoritairement hors reseau.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
2026-08-27 10:33:21 +02:00

85 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

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).

26. Session du 27/08/2026 — L'oscillateur du takeover nuit, et deux acteurs non lus

L'oscillateur (corrigé, golden 40 → 42, déployé 08:32)

Nuit du 26-27/08, Jeep pleine restée branchée, aucun besoin de charge : trois sessions EDF (04:24→05:16, 05:31→06:50, 07:05→07:30), 2,5 kWh importés un matin annoncé à 43 kWh de solaire. La nuit du 20-21/08 en comptait six. Le helper night_grid_takeover_needed précède chaque bascule du contacteur de 3 s : la cause est entièrement dans le moteur de nuit EVCS, pas dans l'hystérésis (grid_soc_on/grid_soc_off valent toujours 20/30, jamais approchés).

Cause, dans WINDOWS_SOURCEV2.js :

gridTakeoverTriggerPct = max(plancher 22, cible 35) = 35   → engage si SOC <= 35
nightRecoveryDone      : SOC >= 35                          → relâche
NIGHT_RECOVERY_REARM_BAND_PCT = 2                           → réengage à 33

Engagement et relâchement sur le même nombre, la bande de 2 points du Fix A ne faisant que retarder le réarmement. Or 2 points de SOC ≈ 0,34 kWh ≈ 31 min à la charge nocturne (~650 W, parfaitement plate) : ce n'est pas une hystérésis, c'est la période d'une horloge. C'est le propriétaire qui a mis le doigt dessus en observant que le phénomène était « super stable car pas de variation de consommation » — la platitude de la charge est ce qui rend le cycle régulier.

Correctif : la recovery batterie post-charge est UNE SEULE passe par session de nuit (nightRecoveryDone = ... || nightRecoveryDonePrev, sans bande). C'était déjà l'intention affichée du Fix A (« relâché DÉFINITIVEMENT »). Aucun seuil de ré-engagement n'est ajouté : batteryBelowHardFloor reste un terme indépendant de nightGridTakeoverNeeded, et l'hystérésis garde sa bande 20/30. Le vrai critère de ré-engagement est un budget d'énergie restant à couvrir — il appartient au bilan énergétique, pas à une constante de plus ici.

Golden : night-recovery-rearm-sous-bande (qui épinglait l'oscillateur) devient night-recovery-done-pas-de-reengagement ; +night-oscillateur-nuit-2627 (le relevé exact de la nuit) ; +night-recovery-done-plancher-dur (le filet de sécurité doit rester : SOC 21 → ré-engagement). Preuve par mutation : bande restaurée → échec du nouveau scénario ; retirée → 42/42.

D'où venait le 35 %, et pourquoi ce n'est pas une réserve

batteryMorningTargetPct vient des règles de prévision (≥ 18 kWh → 35 %, ≥ 10 → 55 %, sinon 85 %). C'est une optimisation économique — « demain il fera beau, la batterie peut descendre » — et non une réserve de sécurité, ce que le propriétaire croyait. Point de conception qui en découle : une réserve contre la perte du réseau ne peut pas être constituée avec le réseau ; elle se charge sur le solaire et se décide le soir. Voir §27 (à écrire) pour la réserve d'orage.

Acteur non lu n°1 — les charges programmées de l'ESS (IMPORTANT)

Le GX a trois fenêtres de charge réseau programmées, actives tous les jours, que rien dans nos workflows ne lit :

# Départ Durée Cible SOC Actif
0 01:35 365 min (→ 07:40) 45 % on
1 07:30 298 min (→ 12:28) 45 % on
2 12:24 120 min (→ 14:24) 55 % on
3 14:24 120 min 85 % off
4 23:08 60 min 50 % off

Entités : switch.gx_device_ess_batterylife_schedule_charge_N_enabled, time..._start, number..._duration, number..._soc. sensor.gx_device_ess_batterylife_state = self_consumption hors réseau. Le GX veut donc 45 % la nuit quand EDF est présent, quand notre cible dit 35 % : deux automates avec deux consignes différentes sur la même batterie. À arbitrer (les désactiver et garder un seul propriétaire, ou les aligner).

Acteur non lu n°2 — l'export d'excédent PV

switch.gx_device_dc_coupled_pv_feed_in_excess = off · switch.gx_device_ac_coupled_pv_feed_in_excess = off · ess_max_feed_in_power = −1. Cohérent avec « sans injection », à confirmer comme voulu.

Ce qui n'est PAS démontré : le bridage du MPPT par la présence d'EDF

Tentative de preuve sur la seule fenêtre EDF-ON diurne conservée en base (21/08, 07:24→08:07) : PV moyenne 91 W (max 218) contre 907 W (max 1464) sur les 43 min suivantes, EDF coupé, à consommation identique (668 vs 637 W). Écart spectaculaire — mais l'enveloppe solaire passe elle-même de 0,17 kWh à 07 h à 1,03 kWh à 08 h, soit un facteur 6 qui explique l'essentiel. La mesure est donc confondue par le lever du soleil et ne prouve rien. Ne pas décider d'une mise à jour firmware sur cette base. Test propre à faire : fermer EDF 10 min en milieu de journée, batterie non pleine, et comparer.

Historique HA plus court que configuré

configuration.yaml déclare purge_keep_days: 90, mais l'historique brut s'arrête au 20/08 (~7 jours) : testé vide au 01/06, 29/07, 10/08, 17/08 ; 1 point au 20/08, 7 au 24/08. Base recréée, ou purge qui ne respecte pas la consigne — à investiguer. Les statistiques long terme (horaires, conservées indéfiniment) sont un stock distinct et probablement intactes.

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

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

25. Session du 24/08/2026 — Fix G retiré + diagnostic des deux oscillateurs

Fix G retiré de la production (déployé 24/08 12:18, golden-hysteresis 7/7)

Fix G (§24) a produit en production un yoyo EDF de 2-4 min la nuit du 23-24/08, véhicule débranché : coupure à SOC 21 % (Fix G, bypass min_on) puis rallumage à SOC 20 % (branche hold_on_low_soc, bypass cooldown), MPPT qui flappe à chaque bascule. Logbook : turn_off_evcs_override_released 06:43:03 → hold_on_low_soc 06:45:03 → turn_on_low_soc_cooldown_bypassed 06:52:03.

Erreur de conception : le garde soc > gridSocOn ne protégeait que l'INSTANT de l'engagement sur SOC bas (soc <= 20), pas la PHASE DE REMONTÉE 20 → 30 qui est 100 % de la durée de vie de ce cas. Fix G a donc remplacé l'hystérésis 20/30 par une hystérésis 20/20,1.

Erreur de test : le fixture tests/golden-hysteresis/ portait grid_soc_on: "22" / grid_soc_off: "35" — les valeurs de repli du code, pas la production (vérifiée via MCP : 20 / 30, inchangées depuis le 07/08). Et le scénario de non-régression g-baseline-low-soc-no-override utilisait SOC 18 %, c'est-à-dire le seul point que Fix G épargnait. → Règle : figer un fixture de test sur des valeurs lues en live via MCP, jamais sur les fallbacks du code.

Retrait : tools/patch-20260824-revert-g.mjs (rejouable) — le jsCode résultant est byte-identique à la version pré-Fix-G (commit 0419c74), vérifié. Suite réécrite (7 scénarios, seuils réels 20/30) avec les garde-fous manquants h-recovery-band-no-cut-21 / -25 : la remontée batterie sur réseau ne doit JAMAIS être interrompue avant grid_soc_off. Test de mutation : Fix G réappliqué → 4/7 échecs, retiré → 7/7. Le filet attrape désormais la régression.

Diagnostic : deux oscillateurs, même pathologie

Détail complet dans docs/MANUEL_COMPLET.md §11.8. Résumé :

A — Yoyo EDF. Le problème de fond n'est pas le réglage de grid_soc_off mais le fait qu'il n'a aucun sens physique : le 24/08, la PV ne couvre la conso maison qu'à partir de ~09:00 (0,07 → 0,63 → 2,74 kWh/h contre 0,75 kWh/h de conso). Couper EDF à 06:43 à 21 % rend le yoyo inévitable quels que soient les seuils. Le workflow hystérésis lit 25 champs et aucune prévision solaire. Cible : session réseau unique dimensionnée jusqu'au croisement PV/conso, arbitrée par le conseiller IA. NB : mesurer la vraie capacité PV impose de couper EDF (sinon le MPPT est bridé et on ne voit rien).

B — Oscillation charge EVCS en plein soleil (24/08 12:25 → 12:28). Ce n'est PAS une limite 5 kVA (aucun ve_bus_overload depuis le 20/08, ac_loads_max_safe_w = 9000, AC-out L1 à 5335 W sans overload, plafond calculé ~20 A) et pas non plus une bande morte batterie trop étroite : le courant batterie était à +127 A au moment de l'arrêt. Les deux atténuations que j'avais d'abord proposées (battery_discharge_soft_a à −12 A, battery_neutral_band_a à 3-5 A) n'avaient donc aucun effet sur ce cas — l'utilisateur l'a signalé, à raison, avant que je les applique.

Cause réelle : inversion du plancher de démarrage et du plancher de maintien. À 12:24:00 le profil bascule vers midday_hc_or_solar ; avec SOC 58 % le plancher de démarrage vaut min(midday_soc_min 75, abondance P6 40) = 40 et le plancher de maintien max(soc_stop_min 29, 75-15) = 60. Le système a le droit de DÉMARRER ce qu'il n'a pas le droit de MAINTENIR → cycle limite à la période de min_run_minutes (3 min) sur toute la bande [40 %, 60 %], et ~2,6 kW de PV écrêtés à chaque creux (PV 10,24 kW → 7,64 kW quand la borne s'arrête).

Décisions de conception validées avec l'utilisateur

  • La conso se lit, elle ne se devine pas ; filtrage numérique dynamique des pointes (improbables la nuit hors charge EVCS ; pas de moyenne quand aucun véhicule n'est branché).
  • SOC : lissage pour les décisions de confort, valeur instantanée conservée pour la sécurité (le risque prime).
  • Les seuils fixes doivent devenir dynamiques (saison, prévision) et être arbitrés par le conseiller IA : « un automatisme c'est trop rigide pour traiter tous ces cas tordus ». Ne plus empiler de branches codées en dur.
  • Exploiter les messages d'état réels du Victron et de la borne au lieu de s'auto-limiter à l'aveugle sur un proxy.

Correctif déployé 24/08 12:45 — invariant maintien <= démarrage

src/nodes/BATTERY_CLOUD_LOCKS.js, deux volets indissociables :

  1. effectiveSocHoldFloor = min(règle du profil, plancher de démarrage effectif) — on ne peut plus être autorisé à démarrer ce qu'on n'est pas autorisé à maintenir. Sans abondance, comportement inchangé.
  2. L'abondance P6 se PROUVE au démarrage (solarAbundanceStart : pvAvg >= 4000 ET batterie >= +20 A) mais se MAINTIENT en charge (solarAbundanceHold : pvAvg >= 4000 ET batterie >= 0). Sans ce dédoublement, la charge du véhicule détourne le surplus, fait tomber le courant batterie sous +20 A et détruit sa propre condition d'autorisation — l'oscillation reviendrait dès que le PV n'est que moyennement fort.

Golden 39 → 40 : midday-inversion-start-hold (relevé VRM du 24/08 12:25 : PV 10,24 kW, charges 3815 W dont EVCS 3502 W, batterie +127 A, SOC 58 %). Preuve par mutation : code d'avant → stop / « Conditions de maintien perdues » ; après → charging_step_up_battery à 19 A. Les 39 scénarios existants inchangés.

Leçon transverse : trois oscillateurs identifiés le même jour (Fix G, abondance P6 auto-destructrice, inversion démarrage/maintien) — même pathologie à chaque fois, des seuils empilés par couches successives, chacun raisonnable isolément, dont personne ne vérifie la cohérence d'ensemble. D'où le chantier du bilan énergétique partagé plutôt qu'un garde de plus.


Document mis à jour le : 24/08/2026 (~12:50) — Fix G retiré (golden-hysteresis 7/7, seuils réels 20/30), inversion démarrage/maintien corrigée (golden 40/40), diagnostic des deux oscillateurs