Files
EVCS/HANDOFF.md
Tallagrand Thierry 9c3a0a9c6e R1: déduplication du moteur nuit — WINDOWS_SOURCEV2 unique calculateur
- Le moteur nuit (161 lignes) était calculé 2x (WINDOWS + BCL), chaque patch
  nuit devait être appliqué 2x (P2, 04/08, P7) -> risque de divergence.
- WINDOWS calcule tout (+ startMs, needsCapture strict, recoveryAllowedNow,
  targetReached) et exporte nightEvcsPreview étendu.
- BCL consomme prev.nightEvcsPreview avec fallbacks sûrs (preview absent ->
  'blocked'). Export nightEvcs inchangé pour DECISION/HA_SERVICE.
- A/B git HEAD vs patché : 7 scénarios + 2 cas limites, sorties strictement
  identiques. Déployé en production, exécutions vérifiées success.
2026-08-07 19:05:21 +02:00

32 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