- Workflow n8n tdfInZns7cqo8fU2 actif (15 min): snapshot HA compact -> prompt FR -> LLM local Spark (modèle dynamique via /v1/models) avec fallback OpenRouter claude-haiku-4.5 (credential chiffré, clé hors git) -> validation stricte (enums, bornes, rejet total si invalide) -> helpers advisor_* + divergence + compteurs accord/divergence + logbook. - GARANTIE OMBRE structurelle: écritures limitées à advisor_*/logbook, vérifiée par tests/advisor/run.mjs (16 tests, bloquants au push). - Kill-switch input_boolean.evcs_advisor_enabled. - Extension R2 multi-workflows (lib-node-map WORKFLOWS[], build/extract/push par fichier, n8n-create.mjs, gen-advisor-workflow.mjs). - Helpers HA via package NFS + carte dashboard EVCS Master (backup fait). - 1re reco réelle: auto_solar_plus, cible 35%, accord avec le réel. - Fix sécurité n8n-push: plus de fallback d'id implicite vers le workflow EVCS.
43 KiB
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(idATswvGfG4KJUls0K) — pilote la borne EVCS (30 s)Victron - Battery / Grid Hysteresis Fix V1(id7iIlu7LLTW7TZA8o) — pilote le switch EDF (1 min)
- Accès :
- HA :
http://10.10.10.17:8123— jeton dansjeton_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 SSEhttps://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).
- HA :
- 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 P1modified/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é)
- Entité fantôme corrigée : le snapshot lisait
input_select.victron_evcs_user_strategy(inexistante → toujoursunknown). Corrigé eninput_select.victron_evcs_strategy. Conséquence avant fix : les overridesforce_grid_now/force_charge_nowet la coupure « voiture pleine » ne fonctionnaient JAMAIS dans l'hystérésis. - Switch EDF
unavailable: nouvel étatswitch_unavailable= aucun ordre, aucun timestamp, réévaluation complète au retour du switch. Avant : spamturn_onchaque 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 %). last_on_at: réécrit uniquement sur vraie transition (latch OFF→ON), plus à chaque retry. Avant :hold_on_min_runtimepouvait durer éternellement.- Bug latent corrigé : le bloc
turn_off_force_grid_vehicle_full(V2.5) était écrasé par la chaîne principale (code mort). FlagforcedCutajouté.
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). nightTakeoverLatcheddansWINDOWS_SOURCEV2etBATTERY_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, etcloudDebten 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(idcTeeHy4ePiVeELLh) est le propriétaire des limites DVCC (MaxChargeCurrent/MaxChargeVoltagevia 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: sinight_grid_takeover_neededON + EDF ON → SOC < cible-1 : recharge 30 A ; sinon maintien 2 A / 50.1 V (maison + VE sur HC, batterie en pause). Cible lue depuisinput_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é). Ajouttomorrow_kwhau 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) : sipvPowerAvg10m ≥ 4000 WetbatteryCurrentA ≥ +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_currentimmé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_ontoutes 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: allactivé 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_1est 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. Backupmodified/GARAGE_backup_*. - Templates MQTT durcis (33 occurrences,
configuration.yaml+packages/victron_vebus_mqtt.yaml) : gardevalue_json is defined→ plus d'erreursvalue_json is undefinedaux reconnexions MQTT. BackupsSAVE_CONFIG/*.bak.20260709-234800,check_configvalid,mqtt.reloadfait. ⚠️ Après unmqtt.reload, les sensors restentunknownjusqu'à republication Venus : publierR/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 :
- Borne fantôme au matin du 08/07 (« véhicule en charge » sans véhicule) +
force_grid_nowde la veille ⇒ EDF maintenu ON (la coupure voiture-pleine était du code mort, cf. P1.1/P1.4). - L'utilisateur coupe le disjoncteur physique ⇒ switch
unavailablependant ~5 h. - Le workflow reste coincé en
hold_on_min_runtime(P1.3) en spammantturn_on(P1.2). - 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égiestop+ hystérésis OFF + latch OFF. - Fenêtres EVCS (helpers HA, corrigées/vérifiées le 09/07) :
night_hc01:24→07:24 (HC réelles),morning_solar07:30→11:00,midday_hc_or_solar12:24→14:24 (HC réelles),evening_high_soc17:00→20:30, sinonoutside_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)
- Backup :
GET /api/v1/workflows/<id>→modified/*_backup_<stamp>.json. - Patch des
jsCodepar remplacement exact (script node),node --checksur chaque node. - Dry-run local de la chaîne complète : snapshot réel via
POST /api/templateHA, exécution séquentielle des 5 nodes avec shim$input, horloge simulable en surchargeantglobal.Date(voir session du 09/07). PUT /api/v1/workflows/<id>(⚠️ filtrersettings: seulsexecutionOrder/timezone/save*/executionTimeout/errorWorkflowsont acceptés).- Vérification live : helpers HA 40-70 s après.
7. Backlog / Point du 10/07 au réveil
- 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_recoverpuisevcs_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_modeentre 01:00 et 08:00.
- 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.
- Baisser
input_number.victron_evcs_soc_start_min94 → 90 (aligner avec P3) si souhaité. - Sécuriser : auth Bearer sur les 5 MCP Server Triggers n8n + révoquer/déplacer le jeton HA.
- À investiguer : « conditions de démarrage nuit batterie non réunies » quand la batterie
suffit pour la nuit (conditions de départ
battery_assist_night). - 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_onconditionné 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 denoDemandScoreOff - Conséquence :
evcsVehicleNoDemandSemanticrestaitfalsemê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_activerestaitondepuis 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_ondéclenchée dèssourceMode=grid_forcedougrid_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 batteriebattery_neutral_band_a: 0A (au lieu de -2A) — tolérance nullesoc_start_min: 80% (au lieu de 94%) — seuil de démarrage plus réalistenight_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. - Ligne ~333 :
⚙️ 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 :8000use-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%) vsforce_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
- Charge HC ne démarre pas à 1h30 : Le grid ne se mettait pas en route à l'ouverture de la fenêtre HC
- 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
gridAllowedBySoc(BATTERY_CLOUD_LOCKS) : autorise le grid seulement sisoc <= 35%ouforceGridRequested. En profilenight_hcavec SOC > 35%, le grid était bloqué.nightTakeoverLatched(WINDOWS_SOURCEV2) : reste latché tant quesoc < 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.mjscréé, 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
battery_assist_nightne démarre jamais :batteryAssistNightStartOkexigebatteryCurrentA >= 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.)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&& !nightGridHcPreferrednightGridTakeoverNeeded: ajoutnightGridHcPreferred ||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) etnode --checksur 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)
- 01:24-01:25 :
source_mode→Réseau HC autorisé, EDF ON, takeover helper ON ; start_grid_hc_min_current→ montée en courant (plafond EDF ON = helper 20 A / boost 28 A) ;- ACM en
evcs_takeover_hold(batterie ~2 A, SOC stable) ; - 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,nightSessionNeedsCaptureversion stricte (critère startMs inclus),batteryRecoveryAllowedNow,batteryTargetReached. ExportnightEvcsPreviewé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
nightEvcsde 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 viatools/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_nowla nuit (toggle HC ON) →start_manual_local_min_currentET takeover helper ON (session nuit active) : EDF peut venir en renfort. Voulu.- Garde SOC>35 midi : sourceMode
grid_hc_allowedmaisidle_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
- 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é. - P7 ne relâchait pas :
nightGridHcPreferredrestait vrai tant que véhicule branché → l'EVCS redemandait EDF ON toutes les 30 s jusqu'à 07:24. - 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)
- 01:24-01:25 : grid_hc_allowed → EDF ON → charge 6 A puis montée (P7)
- 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)
- Batterie sous cible matin (35 été) : EDF maintenu + recharge ACM jusqu'à cible
- 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)
- Template /api/template DOIT être du Jinja :
{{ { ... } | tojson }}— sans wrapper, HA renvoie le texte brut non rendu. - 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.) - MCP activate_workflow → erreur 415 : activer via REST POST .../activate body {}.
- n8n-push : le fallback d'id par défaut (EVCS) était DANGEREUX avec un 2e
workflow → supprimé, l'id vient du champ
iddu 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).
Phase 3 — critères de bascule (dans 2-3 semaines d'observation)
- Taux d'accord stratégie ≥ 80% (compteurs agree/diverge sur la carte) ;
- Zéro sortie invalide NON rejetée (advisor_status) ;
- 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é.