diff --git a/HANDOFF.md b/HANDOFF.md index 7a129e7..5db167e 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -357,6 +357,66 @@ Tous les maillons sont corrigés par P1. - Export du workflow dans `workflows/evcs-surplus-controller-v2.6.json` - Commit & push effectués +## 10. Session du 04/08/2026 (matin) — Fix grid HC 1h30 + libération takeover + +### Problèmes诊断qués +1. **Charge HC ne démarre pas à 1h30** : Le grid ne se mettait pas en route à l'ouverture de la fenêtre HC +2. **Grid ne rend pas la main après charge** : EDF restait ON après la fin de charge (SOC 71% < 90%) + +### Root Causes identifiées +1. **`gridAllowedBySoc`** (BATTERY_CLOUD_LOCKS) : autorise le grid seulement si `soc <= 35%` ou `forceGridRequested`. En profile `night_hc` avec SOC > 35%, le grid était bloqué. +2. **`nightTakeoverLatched`** (WINDOWS_SOURCEV2) : reste latché tant que `soc < 90%` (hardcodé). Pas de condition "véhicule chargé" → le takeover ne se libérait jamais. + +### Corrections appliquées + +#### Fix 1: `gridAllowedBySoc` — Autoriser le grid en profile night_hc +**Node:** `BATTERY_CLOUD_LOCKS` +```javascript +// AVANT: +const gridAllowedBySoc = parsed.soc <= 35 || strategy.forceGridRequested; + +// APRÈS: +const gridAllowedBySoc = + strategy.forceGridRequested || + context.profile === 'night_hc' || + parsed.soc <= 35; +``` + +#### Fix 2: `nightTakeoverLatched` — Libérer si charge terminée +**Node:** `WINDOWS_SOURCEV2` +```javascript +// AVANT: +const nightTakeoverLatched = + nightSessionActive && + nightTakeoverPrev && + parsed.soc < NIGHT_TAKEOVER_RELEASE_SOC_PCT; + +// APRÈS: +const vehicleChargeCompleted = + !parsed.evcsChargingSwitchOn && + parsed.soc >= 50; +const nightTakeoverLatched = + nightSessionActive && + nightTakeoverPrev && + ( + parsed.soc < NIGHT_TAKEOVER_RELEASE_SOC_PCT || + vehicleChargeCompleted + ); +``` + +### Méthode de déploiement +- **MCP n8n-mcp_update_workflow** : Erreur 1003 (request/body must NOT have additional properties) — bug du MCP +- **Bypass** : Script `tools/n8n-rest-update-test.mjs` créé, utilise REST API n8n directement avec token +- PUT réussi avec body minimal : `{name, nodes, connections, settings, staticData}` +- Settings doit être minimal : `{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner"}` + +### Backup +- `backup-workflow-ATswvGfG4KJUls0K-1785849621547.json` + +### Vérification +- Les 2 patches sont dans le workflow en production +- À surveiller : prochaine nuit HC (01:24-07:24) pour valider le comportement + --- -*Document mis à jour le : 18/07/2026 (~03:00) — corrections documentation MANUEL_COMPLET.md, probe 6A→10A, workflow exporté vers git* -*État live à la clôture : Système fonctionnel, probe configuré à 10A* +*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* diff --git a/tools/n8n-rest-update-test.mjs b/tools/n8n-rest-update-test.mjs new file mode 100644 index 0000000..0d56d67 --- /dev/null +++ b/tools/n8n-rest-update-test.mjs @@ -0,0 +1,161 @@ +#!/usr/bin/env node + +const N8N_URL = process.env.N8N_URL || 'https://n8n.airtria.pro'; +const N8N_API_KEY = process.env.N8N_API_KEY; +const WORKFLOW_ID = process.env.WORKFLOW_ID || 'ATswvGfG4KJUls0K'; + +if (!N8N_API_KEY) { + console.error('❌ Erreur: N8N_API_KEY non défini'); + console.error(' Exportez la variable: export N8N_API_KEY="votre_token"'); + process.exit(1); +} + +const headers = { + 'X-N8N-API-KEY': N8N_API_KEY, + 'Content-Type': 'application/json' +}; + +async function main() { + console.log('=== Test PUT workflow n8n ===\n'); + console.log(`URL: ${N8N_URL}/api/v1/workflows/${WORKFLOW_ID}`); + console.log(`Workflow ID: ${WORKFLOW_ID}\n`); + + // 1. Lire le workflow + console.log('1️⃣ Lecture du workflow via GET...'); + let getResponse; + try { + getResponse = await fetch(`${N8N_URL}/api/v1/workflows/${WORKFLOW_ID}`, { + method: 'GET', + headers + }); + } catch (err) { + console.error('❌ Erreur réseau GET:', err.message); + process.exit(1); + } + + if (!getResponse.ok) { + console.error(`❌ GET échoué: ${getResponse.status} ${getResponse.statusText}`); + const text = await getResponse.text(); + console.error('Réponse:', text); + process.exit(1); + } + + const workflow = await getResponse.json(); + console.log(`✅ Workflow lu: "${workflow.name}" (${workflow.nodes?.length || 0} nodes)`); + + // 2. Sauvegarde locale + const backupPath = `./backup-workflow-${WORKFLOW_ID}-${Date.now()}.json`; + console.log(`\n2️⃣ Sauvegarde locale: ${backupPath}`); + const fs = await import('fs'); + fs.writeFileSync(backupPath, JSON.stringify(workflow, null, 2)); + console.log('✅ Backup créé'); + + // 3. Reconstruire body propre (settings minimal) + const body = { + name: workflow.name, + nodes: workflow.nodes, + connections: workflow.connections, + settings: { + executionOrder: workflow.settings?.executionOrder || 'v1', + callerPolicy: workflow.settings?.callerPolicy || 'workflowsFromSameOwner' + }, + staticData: workflow.staticData + }; + + console.log('\n3️⃣ Body reconstruit (champs inclus):'); + console.log(' - name:', body.name); + console.log(' - nodes:', body.nodes?.length || 0); + console.log(' - connections:', body.connections ? 'présent' : 'absent'); + console.log(' - settings:', JSON.stringify(body.settings)); + console.log(' - staticData:', body.staticData ? 'présent' : 'absent'); + + // 4. Appliquer les corrections + console.log('\n4️⃣ Application des corrections...'); + + // Trouver le node BATTERY_CLOUD_LOCKS + const batteryCloudLocksNode = body.nodes.find(n => n.name === 'BATTERY_CLOUD_LOCKS'); + if (!batteryCloudLocksNode) { + console.error('❌ Node BATTERY_CLOUD_LOCKS non trouvé'); + process.exit(1); + } + + let jsCode = batteryCloudLocksNode.parameters.jsCode; + + // Fix 1: gridAllowedBySoc - ajouter context.profile === 'night_hc' + const fix1Old = 'const gridAllowedBySoc = parsed.soc <= 35 || strategy.forceGridRequested;'; + const fix1New = `const gridAllowedBySoc = + strategy.forceGridRequested || + context.profile === 'night_hc' || + parsed.soc <= 35;`; + + if (jsCode.includes(fix1Old)) { + jsCode = jsCode.replace(fix1Old, fix1New); + console.log(' ✅ Fix 1 appliqué: gridAllowedBySoc'); + } else { + console.log(' ⚠️ Fix 1: pattern non trouvé (déjà appliqué?)'); + } + + // Fix 2: nightTakeoverLatched - libérer quand charge terminée (soc >= 50) + const fix2Old = `const nightTakeoverLatched = + nightSessionActive && + nightTakeoverPrev && + parsed.soc < NIGHT_TAKEOVER_RELEASE_SOC_PCT;`; + + const fix2New = `const vehicleChargeCompleted = + !parsed.evcsChargingSwitchOn && + parsed.soc >= 50; +const nightTakeoverLatched = + nightSessionActive && + nightTakeoverPrev && + ( + parsed.soc < NIGHT_TAKEOVER_RELEASE_SOC_PCT || + vehicleChargeCompleted + );`; + + if (jsCode.includes(fix2Old)) { + jsCode = jsCode.replace(fix2Old, fix2New); + console.log(' ✅ Fix 2 appliqué: nightTakeoverLatched'); + } else { + console.log(' ⚠️ Fix 2: pattern non trouvé (déjà appliqué?)'); + } + + batteryCloudLocksNode.parameters.jsCode = jsCode; + + // 5. Tenter PUT + console.log('\n5️⃣ Tentative PUT...'); + let putResponse; + try { + putResponse = await fetch(`${N8N_URL}/api/v1/workflows/${WORKFLOW_ID}`, { + method: 'PUT', + headers, + body: JSON.stringify(body) + }); + } catch (err) { + console.error('❌ Erreur réseau PUT:', err.message); + process.exit(1); + } + + const putStatus = putResponse.status; + const putStatusText = putResponse.statusText; + const putResponseText = await putResponse.text(); + + console.log(`\n📊 Résultat PUT:`); + console.log(` Status: ${putStatus} ${putStatusText}`); + + if (putResponse.ok) { + console.log('✅ PUT réussi!'); + const putJson = JSON.parse(putResponseText); + console.log(` Workflow mis à jour: "${putJson.name}"`); + } else { + console.log('❌ PUT REFUSÉ par n8n'); + console.log('\n📄 Erreur exacte:'); + try { + const errorJson = JSON.parse(putResponseText); + console.log(JSON.stringify(errorJson, null, 2)); + } catch { + console.log(putResponseText); + } + } +} + +main(); \ No newline at end of file