diff --git a/docs/mesures/carte-decisions.json b/docs/mesures/carte-decisions.json new file mode 100644 index 0000000..88b042f --- /dev/null +++ b/docs/mesures/carte-decisions.json @@ -0,0 +1,554 @@ +{ + "genere": "2026-09-12T05:13:34.948Z", + "carte": { + "switch.edf_general_ok_switch": { + "libelle": "Contacteur reseau EDF", + "ecrivains": [ + { + "famille": "n8n", + "nom": "Victron - Battery / Grid Hysteresis Fix V1", + "id": "7iIlu7LLTW7TZA8o", + "services": [ + "turn_on", + "turn_off" + ], + "desactive": false + }, + { + "famille": "n8n", + "nom": "HA Snapshot EVCS Surplus Controller V2.6", + "id": "ATswvGfG4KJUls0K", + "services": [ + "turn_on", + "set_value", + "turn_off" + ], + "desactive": false + }, + { + "famille": "HA", + "nom": "automations.yaml", + "services": [ + "turn_on", + "turn_off" + ], + "desactive": false + }, + { + "famille": "HA", + "nom": "scripts.yaml", + "services": [ + "turn_on", + "turn_off", + "set_value" + ], + "desactive": false + } + ], + "lecteurs": [ + "n8n:SDB Climate & Surplus Manager V2.2", + "n8n:Water Heater Advisor V2.3", + "n8n:Victron - Garage Thermal Surplus Manager V2.2", + "n8n:EVCS Energy Balance V1", + "n8n:Victron - Flexible Load Advisor V1", + "n8n:Victron - Auto Charge Manager V1.2", + "n8n:EVCS Advisor Shadow V1", + "n8n:Water Heater Advisor ECS 6 Nodes + Build + Execute" + ] + }, + "number.gx_device_ac_power_setpoint": { + "libelle": "Consigne de puissance reseau", + "ecrivains": [ + { + "famille": "n8n", + "nom": "HA Snapshot EVCS Surplus Controller V2.6", + "id": "ATswvGfG4KJUls0K", + "services": [ + "turn_on", + "set_value", + "turn_off" + ], + "desactive": false + } + ], + "lecteurs": [ + "n8n:EVCS Advisor Shadow V1" + ] + }, + "switch.victron_evcs_charging": { + "libelle": "Charge de la borne", + "ecrivains": [ + { + "famille": "n8n", + "nom": "HA Snapshot EVCS Surplus Controller V2.6", + "id": "ATswvGfG4KJUls0K", + "services": [ + "turn_off", + "turn_on", + "set_current" + ], + "desactive": false + } + ], + "lecteurs": [ + "n8n:Victron - Battery / Grid Hysteresis Fix V1", + "n8n:EVCS Advisor Shadow V1" + ] + }, + "number.victron_evcs_set_current_command": { + "libelle": "Courant de la borne", + "ecrivains": [ + { + "famille": "n8n", + "nom": "HA Snapshot EVCS Surplus Controller V2.6", + "id": "ATswvGfG4KJUls0K", + "services": [ + "turn_on", + "turn_off", + "set_current", + "set_value" + ], + "desactive": false + }, + { + "famille": "HA", + "nom": "scripts.yaml", + "services": [ + "set_value", + "set_current" + ], + "desactive": false + } + ], + "lecteurs": [] + }, + "input_boolean.victron_grid_hysteresis_latch": { + "libelle": "Verrou hysteresis", + "ecrivains": [ + { + "famille": "n8n", + "nom": "Victron - Battery / Grid Hysteresis Fix V1", + "id": "7iIlu7LLTW7TZA8o", + "services": [ + "set_value", + "turn_on", + "turn_off" + ], + "desactive": false + }, + { + "famille": "HA", + "nom": "scripts.yaml", + "services": [ + "turn_off", + "set_value", + "turn_on" + ], + "desactive": false + } + ], + "lecteurs": [ + "n8n:EVCS Advisor Shadow V1" + ] + }, + "input_boolean.victron_evcs_night_grid_takeover_needed": { + "libelle": "Reprise reseau nuit", + "ecrivains": [ + { + "famille": "n8n", + "nom": "HA Snapshot EVCS Surplus Controller V2.6", + "id": "ATswvGfG4KJUls0K", + "services": [ + "turn_on", + "turn_off" + ], + "desactive": false + } + ], + "lecteurs": [ + "n8n:Victron - Battery / Grid Hysteresis Fix V1", + "n8n:Victron - Auto Charge Manager V1.2", + "n8n:EVCS Advisor Shadow V1" + ] + }, + "number.gx_device_ess_min_soc_limit": { + "libelle": "SOC minimal de l ESS", + "ecrivains": [], + "lecteurs": [] + } + }, + "seuilsSoc": { + "20": [ + { + "valeur": 20, + "extrait": "SOC <= 20)", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 20, + "extrait": "SOC <= 20)", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 20, + "extrait": "soc < 20 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 20, + "extrait": "soc < 20 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + } + ], + "22": [ + { + "valeur": 22, + "extrait": "soc < 22)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 22, + "extrait": "SOC < 22%", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 22, + "extrait": "soc < 22)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 22, + "extrait": "SOC < 22%", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 22, + "extrait": "SoC < 22%", + "source": "HA:automations.yaml" + }, + { + "valeur": 22, + "extrait": "SoC < 22%", + "source": "HA:automations.yaml" + }, + { + "valeur": 22, + "extrait": "below: 22", + "source": "HA:automations.yaml" + }, + { + "valeur": 22, + "extrait": "below: 22", + "source": "HA:automations.yaml" + } + ], + "25": [ + { + "valeur": 25, + "extrait": "soc > 25 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 25, + "extrait": "soc > 25 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + } + ], + "35": [ + { + "valeur": 35, + "extrait": "SOC > 35%", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 35, + "extrait": "soc <= 35;", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 35, + "extrait": "SOC > 35%", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 35, + "extrait": "soc <= 35;", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 35, + "extrait": "SoC > 35%", + "source": "HA:automations.yaml" + }, + { + "valeur": 35, + "extrait": "above: 35", + "source": "HA:automations.yaml" + } + ], + "40": [ + { + "valeur": 40, + "extrait": "soc > 40 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 40, + "extrait": "soc >= 40 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 40, + "extrait": "soc >= 40 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 40, + "extrait": "soc > 40 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 40, + "extrait": "soc >= 40 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 40, + "extrait": "soc >= 40 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + } + ], + "60": [ + { + "valeur": 60, + "extrait": "soc > 60 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 60, + "extrait": "soc >= 60 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 60, + "extrait": "soc >= 60 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 60, + "extrait": "soc > 60 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 60, + "extrait": "soc >= 60 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 60, + "extrait": "soc >= 60 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + } + ], + "80": [ + { + "valeur": 80, + "extrait": "soc > 80 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 80, + "extrait": "soc >= 80 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 80, + "extrait": "soc >= 80 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 80, + "extrait": "soc >= 80 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 80, + "extrait": "soc > 80 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 80, + "extrait": "soc >= 80 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 80, + "extrait": "soc >= 80 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 80, + "extrait": "soc >= 80 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 80, + "extrait": "soc < 80)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 80, + "extrait": "soc < 80)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + } + ], + "85": [ + { + "valeur": 85, + "extrait": "soc >= 85 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 85, + "extrait": "soc >= 85 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 85, + "extrait": "soc < 85 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 85, + "extrait": "soc < 85 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + } + ], + "90": [ + { + "valeur": 90, + "extrait": "soc > 90 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "SOC > 90,", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "soc >= 90 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "soc >= 90 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "SOC >= 90\\", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "soc > 90 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "SOC > 90,", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "soc >= 90 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "soc >= 90 ", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "SOC >= 90\\", + "source": "n8n:HA Snapshot EVCS Surplus Controller V2.6" + }, + { + "valeur": 90, + "extrait": "soc < 90)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 90, + "extrait": "soc >= 90)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 90, + "extrait": "soc >= 90 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 90, + "extrait": "soc < 90)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 90, + "extrait": "soc >= 90)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 90, + "extrait": "soc >= 90 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + } + ], + "95": [ + { + "valeur": 95, + "extrait": "soc < 95)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 95, + "extrait": "soc < 95 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 95, + "extrait": "soc < 95 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 95, + "extrait": "soc < 95)", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 95, + "extrait": "soc < 95 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + }, + { + "valeur": 95, + "extrait": "soc < 95 ", + "source": "n8n:Victron - Auto Charge Manager V1.2" + } + ], + "100": [ + { + "valeur": 100, + "extrait": "SocAtStartPct <= 100,", + "source": "n8n:EVCS Energy Balance V1" + }, + { + "valeur": 100, + "extrait": "SocAtStartPct <= 100,", + "source": "n8n:EVCS Energy Balance V1" + } + ] + } +} \ No newline at end of file diff --git a/tools/carte-decisions.mjs b/tools/carte-decisions.mjs new file mode 100644 index 0000000..1262f99 --- /dev/null +++ b/tools/carte-decisions.mjs @@ -0,0 +1,160 @@ +#!/usr/bin/env node +// CARTE DES DECISIONS — qui ecrit sur quoi, et avec quels seuils figes. +// +// Ce que le 12/09 a montre : cinq automates ecrivent sur le contacteur reseau, +// chacun localement correct, aucun ne voit les autres, et personne n avait la +// liste. Une automatisation Home Assistant coupait a 35 % de SOC pendant que le +// bilan energetique demandait 42,8 % — le conflit a dure sept mois sans etre vu. +// +// Cette carte est GENEREE depuis les sources, jamais tenue a la main : une carte +// maintenue a la main pourrit, et c est precisement ce qu on veut eviter. +// +// node tools/carte-decisions.mjs carte + ecart depuis la derniere fois +// node tools/carte-decisions.mjs --json sortie machine (entree du conseiller) +import fs from 'fs'; +import os from 'os'; +import path from 'path'; + +const HA = '/mnt/ARCH3/docker/HA'; +const INSTANTANE = 'docs/mesures/carte-decisions.json'; + +// Actionneurs dont une ecriture concurrente a une consequence physique. +const ACTIONNEURS = [ + ['switch.edf_general_ok_switch', 'Contacteur reseau EDF'], + ['number.gx_device_ac_power_setpoint', 'Consigne de puissance reseau'], + ['switch.victron_evcs_charging', 'Charge de la borne'], + ['number.victron_evcs_set_current_command', 'Courant de la borne'], + ['input_boolean.victron_grid_hysteresis_latch', 'Verrou hysteresis'], + ['input_boolean.victron_evcs_night_grid_takeover_needed', 'Reprise reseau nuit'], + ['number.gx_device_ess_min_soc_limit', 'SOC minimal de l ESS'], +]; + +function cleApi() { + const p = path.join(os.homedir(), '.config/opencode/opencode.json'); + const c = JSON.parse(fs.readFileSync(p, 'utf8')); + const e = c?.mcp?.['n8n-mcp']?.environment || {}; + return { url: (e.N8N_API_URL || '').replace(/\/api\/v1\/?$/, ''), key: e.N8N_API_KEY }; +} + +// Une ECRITURE, pas une lecture : l entite apparait pres d un appel de service. +function ecrit(texte, entite) { + const i = texte.indexOf(entite); + if (i === -1) return null; + const services = []; + let p = 0; + while (true) { + const j = texte.indexOf(entite, p); + if (j === -1) break; + const autour = texte.slice(Math.max(0, j - 400), j + 400); + for (const m of autour.matchAll(/(turn_on|turn_off|set_value|toggle|set_current)/g)) services.push(m[1]); + p = j + 1; + } + return services.length ? [...new Set(services)] : null; +} + +// Seuils numeriques compares a un SOC : la famille de valeurs qui a produit +// l incident. On les remonte tous, avec leur emplacement. +function seuilsSoc(texte, source) { + const trouves = []; + const motifs = [ + /soc\w*\s*(?:<=?|>=?|===?)\s*(\d{1,3})(?:\D|$)/gi, + /(\d{1,3})\s*(?:<=?|>=?)\s*\w*soc/gi, + /\babove:\s*(\d{1,3})\b/gi, + /\bbelow:\s*(\d{1,3})\b/gi, + ]; + for (const re of motifs) { + for (const m of texte.matchAll(re)) { + const v = Number(m[1]); + if (v >= 5 && v <= 100) trouves.push({ valeur: v, extrait: m[0].replace(/\s+/g, ' ').slice(0, 60), source }); + } + } + return trouves; +} + +const sources = []; + +// ── n8n ─────────────────────────────────────────────────────────────── +const { url, key } = cleApi(); +const r = await fetch(`${url}/api/v1/workflows?limit=100&active=true`, { headers: { 'X-N8N-API-KEY': key } }); +for (const w of (await r.json()).data) { + sources.push({ famille: 'n8n', nom: w.name, id: w.id, texte: JSON.stringify(w) }); +} + +// ── Home Assistant ──────────────────────────────────────────────────── +for (const f of ['automations.yaml', 'scripts.yaml']) { + const p = path.join(HA, f); + if (fs.existsSync(p)) sources.push({ famille: 'HA', nom: f, texte: fs.readFileSync(p, 'utf8') }); +} +const paquets = path.join(HA, 'packages'); +if (fs.existsSync(paquets)) { + for (const f of fs.readdirSync(paquets).filter(x => x.endsWith('.yaml'))) { + sources.push({ famille: 'HA', nom: `packages/${f}`, texte: fs.readFileSync(path.join(paquets, f), 'utf8') }); + } +} + +// ── Carte ───────────────────────────────────────────────────────────── +const carte = {}; +for (const [entite, libelle] of ACTIONNEURS) { + carte[entite] = { libelle, ecrivains: [], lecteurs: [] }; + for (const s of sources) { + if (!s.texte.includes(entite)) continue; + const services = ecrit(s.texte, entite); + // Un commentaire de desactivation ne compte pas comme un ecrivain. + const desactive = /#\s*(DESACTIVEE|DISABLED)/i.test(s.texte) && s.famille === 'HA'; + if (services) carte[entite].ecrivains.push({ famille: s.famille, nom: s.nom, id: s.id, services, desactive }); + else carte[entite].lecteurs.push(`${s.famille}:${s.nom}`); + } +} + +const seuils = []; +for (const s of sources) seuils.push(...seuilsSoc(s.texte, `${s.famille}:${s.nom}`)); +const parValeur = {}; +for (const t of seuils) (parValeur[t.valeur] ||= []).push(t); + +const resultat = { genere: new Date().toISOString(), carte, seuilsSoc: parValeur }; + +if (process.argv.includes('--json')) { + console.log(JSON.stringify(resultat, null, 2)); +} else { + console.log('═══ QUI ECRIT SUR QUOI ═══\n'); + for (const [entite, d] of Object.entries(carte)) { + const actifs = d.ecrivains.filter(e => !e.desactive); + console.log(`${d.libelle}`); + console.log(` ${entite}`); + if (!actifs.length) console.log(' (aucun ecrivain detecte)'); + for (const e of actifs) { + console.log(` ✎ ${e.famille.padEnd(4)} ${e.nom}${e.id ? ` (${e.id})` : ''} [${e.services.join(', ')}]`); + } + for (const e of d.ecrivains.filter(x => x.desactive)) { + console.log(` ⊘ ${e.famille.padEnd(4)} ${e.nom} — desactive`); + } + if (actifs.length > 1) console.log(` ⚠️ ${actifs.length} ECRIVAINS CONCURRENTS`); + console.log(` ${d.lecteurs.length} lecteur(s)\n`); + } + console.log('═══ SEUILS DE SOC FIGES ═══\n'); + for (const v of Object.keys(parValeur).map(Number).sort((a, b) => a - b)) { + const t = parValeur[v]; + const oú = [...new Set(t.map(x => x.source))]; + console.log(` ${String(v).padStart(3)} % ×${String(t.length).padStart(2)} ${oú.slice(0, 3).join(', ')}${oú.length > 3 ? ` +${oú.length - 3}` : ''}`); + } +} + +// ── Ecart depuis la derniere fois : c est ca, la trace des evolutions ── +if (!process.argv.includes('--json')) { + if (fs.existsSync(INSTANTANE)) { + const av = JSON.parse(fs.readFileSync(INSTANTANE, 'utf8')); + const cle = c => Object.entries(c.carte).flatMap(([e, d]) => + d.ecrivains.filter(x => !x.desactive).map(x => `${e} <- ${x.famille}:${x.nom}`)); + const avant = new Set(cle(av)), apres = new Set(cle(resultat)); + const nouveaux = [...apres].filter(x => !avant.has(x)); + const partis = [...avant].filter(x => !apres.has(x)); + console.log(`\n═══ ECART DEPUIS ${av.genere.slice(0, 16).replace('T', ' ')} ═══`); + if (!nouveaux.length && !partis.length) console.log(' aucun changement d ecrivain'); + for (const x of nouveaux) console.log(` + ${x}`); + for (const x of partis) console.log(` − ${x}`); + } else { + console.log('\n(premier instantane — l ecart sera affiche aux executions suivantes)'); + } +} +fs.mkdirSync(path.dirname(INSTANTANE), { recursive: true }); +fs.writeFileSync(INSTANTANE, JSON.stringify(resultat, null, 2));