La carte declare un proprietaire, et le conseiller la lit

Essai du 12/09 : avec la seule liste des ecrivains, le conseiller voyait
le conflit mais accusait les deux workflows n8n — l intrus etait une
automatisation Home Assistant. La carte sait lire QUI PEUT ecrire, elle
ne peut pas deviner QUI DEVRAIT : le proprietaire est une decision
humaine, desormais declaree et commentee dans l outil.

Resultat : 3 ecrivains hors proprietaire sur le contacteur reseau, 1 sur
le courant de la borne, 1 sur le verrou. Injectee dans le briefing entre
bornes explicites, regeneree par --injecte.

Le conseiller enonce maintenant la bonne forme (« contradiction entre le
proprietaire et un autre ecrivain ») mais ne nomme toujours pas lequel :
il lui manque l ATTRIBUTION — rien ne relie une bascule a son auteur. Le
contexte Home Assistant distingue un appel de service d une
automatisation ; c est la prochaine piste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-12 07:20:24 +02:00
parent 23dc09a344
commit 6a647dc685
4 changed files with 108 additions and 12 deletions

View File

@@ -1,8 +1,10 @@
{ {
"genere": "2026-09-12T05:13:34.948Z", "genere": "2026-09-12T05:19:01.112Z",
"carte": { "carte": {
"switch.edf_general_ok_switch": { "switch.edf_general_ok_switch": {
"libelle": "Contacteur reseau EDF", "libelle": "Contacteur reseau EDF",
"proprietaire": "Victron - Battery / Grid Hysteresis Fix V1",
"note": "seul cet automate ouvre et ferme le contacteur ; le controleur EVCS le DEMANDE via le helper de reprise nuit, les scripts sont des interventions manuelles assumees",
"ecrivains": [ "ecrivains": [
{ {
"famille": "n8n", "famille": "n8n",
@@ -58,6 +60,8 @@
}, },
"number.gx_device_ac_power_setpoint": { "number.gx_device_ac_power_setpoint": {
"libelle": "Consigne de puissance reseau", "libelle": "Consigne de puissance reseau",
"proprietaire": "HA Snapshot EVCS Surplus Controller V2.6",
"note": "depuis le 01/09 : fermer le contacteur ne suffit pas, il faut demander la puissance",
"ecrivains": [ "ecrivains": [
{ {
"famille": "n8n", "famille": "n8n",
@@ -77,6 +81,8 @@
}, },
"switch.victron_evcs_charging": { "switch.victron_evcs_charging": {
"libelle": "Charge de la borne", "libelle": "Charge de la borne",
"proprietaire": "HA Snapshot EVCS Surplus Controller V2.6",
"note": "proprietaire unique",
"ecrivains": [ "ecrivains": [
{ {
"famille": "n8n", "famille": "n8n",
@@ -97,6 +103,8 @@
}, },
"number.victron_evcs_set_current_command": { "number.victron_evcs_set_current_command": {
"libelle": "Courant de la borne", "libelle": "Courant de la borne",
"proprietaire": "HA Snapshot EVCS Surplus Controller V2.6",
"note": "les scripts BOOST sont des interventions manuelles, reecrites au cycle suivant",
"ecrivains": [ "ecrivains": [
{ {
"famille": "n8n", "famille": "n8n",
@@ -124,6 +132,8 @@
}, },
"input_boolean.victron_grid_hysteresis_latch": { "input_boolean.victron_grid_hysteresis_latch": {
"libelle": "Verrou hysteresis", "libelle": "Verrou hysteresis",
"proprietaire": "Victron - Battery / Grid Hysteresis Fix V1",
"note": "les scripts d urgence le forcent volontairement",
"ecrivains": [ "ecrivains": [
{ {
"famille": "n8n", "famille": "n8n",
@@ -153,6 +163,8 @@
}, },
"input_boolean.victron_evcs_night_grid_takeover_needed": { "input_boolean.victron_evcs_night_grid_takeover_needed": {
"libelle": "Reprise reseau nuit", "libelle": "Reprise reseau nuit",
"proprietaire": "HA Snapshot EVCS Surplus Controller V2.6",
"note": "proprietaire unique",
"ecrivains": [ "ecrivains": [
{ {
"famille": "n8n", "famille": "n8n",
@@ -173,6 +185,8 @@
}, },
"number.gx_device_ess_min_soc_limit": { "number.gx_device_ess_min_soc_limit": {
"libelle": "SOC minimal de l ESS", "libelle": "SOC minimal de l ESS",
"proprietaire": null,
"note": "aucun automate ne devrait y toucher — reglage GX",
"ecrivains": [], "ecrivains": [],
"lecteurs": [] "lecteurs": []
} }

View File

@@ -182,6 +182,16 @@ const systemPrompt = [
' le SOC parait confortable et meme hors heures creuses. Le proprietaire a un', ' le SOC parait confortable et meme hors heures creuses. Le proprietaire a un',
' abonnement 12 kVA et prefere payer quelques kWh plutot que fragiliser le parc.', ' abonnement 12 kVA et prefere payer quelques kWh plutot que fragiliser le parc.',
'', '',
// <<< CARTE GENEREE — ne pas editer a la main, voir tools/carte-decisions.mjs
'QUI ECRIT SUR QUOI (carte generee depuis les sources) :',
'- Contacteur reseau EDF : proprietaire = Battery / Grid Hysteresis Fix V1, AUTRES ECRIVAINS : EVCS Surplus Controller V2.6, automations, scripts',
'- Courant de la borne : proprietaire = EVCS Surplus Controller V2.6, AUTRES ECRIVAINS : scripts',
'- Verrou hysteresis : proprietaire = Battery / Grid Hysteresis Fix V1, AUTRES ECRIVAINS : scripts',
'SEUILS DE SOC FIGES dans le code : 20, 22, 25, 35, 40, 60, 80, 85, 90, 95, 100 % — chacun peut contredire le bilan.',
'Quand un actionneur bascule de facon incoherente, le coupable le plus probable',
'est un ecrivain AUTRE QUE SON PROPRIETAIRE. Nomme-le.',
// CARTE GENEREE >>>
'',
'STRATEGIES POSSIBLES :', 'STRATEGIES POSSIBLES :',
'- auto_solar_plus : automatique, solaire prioritaire, reseau en heures creuses si besoin', '- auto_solar_plus : automatique, solaire prioritaire, reseau en heures creuses si besoin',
'- auto_solar : solaire strict, jamais de reseau', '- auto_solar : solaire strict, jamais de reseau',

View File

@@ -47,6 +47,7 @@ const BASE = {
grid_setpoint_w: '700', grid_setpoint_w: '700',
mppt_operation_modes: 'off,off,off,off,off,off', mppt_operation_modes: 'off,off,off,off,off,off',
vehicle_selected: 'VW', vehicle_selected: 'VW',
evenements_recents: '',
}; };
const LIM = Array(6).fill('voltage_current_limited').join(','); const LIM = Array(6).fill('voltage_current_limited').join(',');
const MAX = Array(6).fill('mppt_active').join(','); const MAX = Array(6).fill('mppt_active').join(',');
@@ -223,6 +224,27 @@ export const CAS = [
modules_en_service:'16', bms_max_discharge_current_a:'400', modules_en_service:'16', bms_max_discharge_current_a:'400',
bms_max_charge_current_a:'400' } }, bms_max_charge_current_a:'400' } },
{ id:'15-oscillation-contacteur', t:'2026-09-12T06:36:00+02:00', certain:true,
titre:"INCIDENT 12/09 06:25-06:35 — le contacteur bascule six fois en dix minutes",
attendu:"REPERER : gravite haute, nommer le conflit entre ecrivains",
s:{ soc_percent:'35', battery_current_a:'20', pv_power_w:'0', house_ac_power_w:'950',
grid_state:'on', acin1_available:'1', night_target_soc_actual:'42.8',
balance_required_soc:'42.8', balance_required_soc_prudent:'49',
balance_energy_to_cover_kwh:'1.56', balance_grid_needed:'on',
balance_grid_energy_needed_kwh:'1.1', balance_crossing_hour:'09 h',
grid_setpoint_w:'5759', tomorrow_kwh:'75.2',
evenements_recents:'06:35:06 Contacteur EDF : off -> on|06:34:02 Contacteur EDF : on -> off|06:30:49 Contacteur EDF : off -> on|06:29:44 Contacteur EDF : on -> off|06:26:31 Contacteur EDF : off -> on|06:25:27 Contacteur EDF : on -> off' } },
{ id:'16-systeme-calme', t:'2026-09-12T07:00:00+02:00', certain:true,
titre:"CONTREFACTUEL — meme nuit, contacteur stable depuis 90 minutes",
attendu:"NE RIEN SIGNALER : gravite aucune",
s:{ soc_percent:'42', battery_current_a:'20', pv_power_w:'0', house_ac_power_w:'950',
grid_state:'on', acin1_available:'1', night_target_soc_actual:'42.8',
balance_required_soc:'42.8', balance_required_soc_prudent:'49',
balance_energy_to_cover_kwh:'0.9', balance_grid_needed:'on',
balance_crossing_hour:'09 h', grid_setpoint_w:'5759', tomorrow_kwh:'75.2',
evenements_recents:'05:02:51 Contacteur EDF : off -> on|05:02:51 Reprise reseau nuit : off -> on' } },
{ id:'14-bilan-indisponible', t:'2026-09-12T04:00:00+02:00', certain:true, { id:'14-bilan-indisponible', t:'2026-09-12T04:00:00+02:00', certain:true,
titre:"ROBUSTESSE — meme nuit que le cas 1 mais bilan DEGRADE", titre:"ROBUSTESSE — meme nuit que le cas 1 mais bilan DEGRADE",
attendu:"Le dire dans sa raison et rester prudent", attendu:"Le dire dans sa raison et rester prudent",
@@ -257,7 +279,8 @@ for (const cas of CAS) {
console.log(` attendu : ${cas.attendu}`); console.log(` attendu : ${cas.attendu}`);
for (const x of passages) { for (const x of passages) {
console.log(x.erreur ? ` ✖ ${x.erreur}` console.log(x.erreur ? ` ✖ ${x.erreur}`
: ` ${String(x.strategy).padEnd(17)} cible ${String(x.night_target_soc).padStart(3)} % ${String(x.current_max_a).padStart(2)} A VE:${String(x.ev_charge_tonight).padEnd(11)} [${Math.round((x.confidence||0)*100)}%] ${String(x.reason).slice(0,110)}`); : ` [${String(x.gravite).padEnd(7)}] ${String(x.constat).slice(0,88).padEnd(90)} ${String(x.strategy).padEnd(16)} cible ${String(x.night_target_soc).padStart(3)}`);
if (!x.erreur && x.ce_qui_cloche) console.log(` ↳ ${String(x.ce_qui_cloche).slice(0,120)}`);
} }
if (ecart !== null) console.log(` → ecart de cible entre passages : ${ecart} pts`); if (ecart !== null) console.log(` → ecart de cible entre passages : ${ecart} pts`);
} }

View File

@@ -19,14 +19,31 @@ const HA = '/mnt/ARCH3/docker/HA';
const INSTANTANE = 'docs/mesures/carte-decisions.json'; const INSTANTANE = 'docs/mesures/carte-decisions.json';
// Actionneurs dont une ecriture concurrente a une consequence physique. // Actionneurs dont une ecriture concurrente a une consequence physique.
// ⚠️ LE PROPRIETAIRE EST UNE DECISION HUMAINE, PAS UNE DEDUCTION.
// La carte sait lire QUI PEUT ecrire ; elle ne peut pas deviner QUI DEVRAIT.
// Sans cette declaration, le conseiller voit bien le conflit mais designe le
// mauvais coupable — essai du 12/09 : il a accuse les deux workflows n8n alors
// que l intrus etait une automatisation Home Assistant.
// Un ecrivain hors de cette liste n est pas forcement fautif : il est A
// EXPLIQUER. C est precisement ce qui manquait pendant sept mois.
const ACTIONNEURS = [ const ACTIONNEURS = [
['switch.edf_general_ok_switch', 'Contacteur reseau EDF'], ['switch.edf_general_ok_switch', 'Contacteur reseau EDF',
['number.gx_device_ac_power_setpoint', 'Consigne de puissance reseau'], 'Victron - Battery / Grid Hysteresis Fix V1',
['switch.victron_evcs_charging', 'Charge de la borne'], 'seul cet automate ouvre et ferme le contacteur ; le controleur EVCS le DEMANDE via le helper de reprise nuit, les scripts sont des interventions manuelles assumees'],
['number.victron_evcs_set_current_command', 'Courant de la borne'], ['number.gx_device_ac_power_setpoint', 'Consigne de puissance reseau',
['input_boolean.victron_grid_hysteresis_latch', 'Verrou hysteresis'], 'HA Snapshot EVCS Surplus Controller V2.6',
['input_boolean.victron_evcs_night_grid_takeover_needed', 'Reprise reseau nuit'], 'depuis le 01/09 : fermer le contacteur ne suffit pas, il faut demander la puissance'],
['number.gx_device_ess_min_soc_limit', 'SOC minimal de l ESS'], ['switch.victron_evcs_charging', 'Charge de la borne',
'HA Snapshot EVCS Surplus Controller V2.6', 'proprietaire unique'],
['number.victron_evcs_set_current_command', 'Courant de la borne',
'HA Snapshot EVCS Surplus Controller V2.6',
'les scripts BOOST sont des interventions manuelles, reecrites au cycle suivant'],
['input_boolean.victron_grid_hysteresis_latch', 'Verrou hysteresis',
'Victron - Battery / Grid Hysteresis Fix V1', 'les scripts d urgence le forcent volontairement'],
['input_boolean.victron_evcs_night_grid_takeover_needed', 'Reprise reseau nuit',
'HA Snapshot EVCS Surplus Controller V2.6', 'proprietaire unique'],
['number.gx_device_ess_min_soc_limit', 'SOC minimal de l ESS',
null, 'aucun automate ne devrait y toucher — reglage GX'],
]; ];
function cleApi() { function cleApi() {
@@ -94,8 +111,8 @@ if (fs.existsSync(paquets)) {
// ── Carte ───────────────────────────────────────────────────────────── // ── Carte ─────────────────────────────────────────────────────────────
const carte = {}; const carte = {};
for (const [entite, libelle] of ACTIONNEURS) { for (const [entite, libelle, proprietaire, note] of ACTIONNEURS) {
carte[entite] = { libelle, ecrivains: [], lecteurs: [] }; carte[entite] = { libelle, proprietaire, note, ecrivains: [], lecteurs: [] };
for (const s of sources) { for (const s of sources) {
if (!s.texte.includes(entite)) continue; if (!s.texte.includes(entite)) continue;
const services = ecrit(s.texte, entite); const services = ecrit(s.texte, entite);
@@ -128,7 +145,9 @@ if (process.argv.includes('--json')) {
for (const e of d.ecrivains.filter(x => x.desactive)) { for (const e of d.ecrivains.filter(x => x.desactive)) {
console.log(` ⊘ ${e.famille.padEnd(4)} ${e.nom} — desactive`); console.log(` ⊘ ${e.famille.padEnd(4)} ${e.nom} — desactive`);
} }
if (actifs.length > 1) console.log(` ⚠️ ${actifs.length} ECRIVAINS CONCURRENTS`); console.log(` proprietaire declare : ${d.proprietaire || 'aucun'}`);
const intrus = actifs.filter(e => e.nom !== d.proprietaire);
if (intrus.length) console.log(` ⚠️ ${intrus.length} ECRIVAIN(S) HORS PROPRIETAIRE : ${intrus.map(e => e.nom).join(', ')}`);
console.log(` ${d.lecteurs.length} lecteur(s)\n`); console.log(` ${d.lecteurs.length} lecteur(s)\n`);
} }
console.log('═══ SEUILS DE SOC FIGES ═══\n'); console.log('═══ SEUILS DE SOC FIGES ═══\n');
@@ -156,5 +175,35 @@ if (!process.argv.includes('--json')) {
console.log('\n(premier instantane — l ecart sera affiche aux executions suivantes)'); console.log('\n(premier instantane — l ecart sera affiche aux executions suivantes)');
} }
} }
// ── Injection dans le briefing du conseiller ──────────────────────────
// La carte est STRUCTURELLE : elle change quand on ajoute un automate, pas
// toutes les 15 minutes. Elle a donc sa place dans le prompt, regeneree ici
// plutot que transmise a chaque cycle. Bornes explicites pour qu une relecture
// voie tout de suite ce qui est genere et ce qui est ecrit a la main.
if (process.argv.includes('--injecte')) {
const P = 'src/advisor/BUILD_PROMPT.js';
const DEBUT = ' // <<< CARTE GENEREE — ne pas editer a la main, voir tools/carte-decisions.mjs';
const FIN = ' // CARTE GENEREE >>>';
const lignes = [" 'QUI ECRIT SUR QUOI (carte generee depuis les sources) :',"];
for (const [entite, d] of Object.entries(carte)) {
const actifs = d.ecrivains.filter(e => !e.desactive);
if (actifs.length < 2) continue;
const court = n => n.replace(/\.yaml$/, '').replace(/^(HA Snapshot |Victron - )/, '');
const intrus = actifs.filter(e => e.nom !== d.proprietaire).map(e => court(e.nom));
lignes.push(` '- ${d.libelle} : proprietaire = ${court(d.proprietaire || 'aucun')}${intrus.length ? `, AUTRES ECRIVAINS : ${intrus.join(', ')}` : ''}',`);
}
const vals = Object.keys(parValeur).map(Number).sort((a, b) => a - b);
lignes.push(` 'SEUILS DE SOC FIGES dans le code : ${vals.join(', ')} % — chacun peut contredire le bilan.',`);
lignes.push(" 'Quand un actionneur bascule de facon incoherente, le coupable le plus probable',");
lignes.push(" 'est un ecrivain AUTRE QUE SON PROPRIETAIRE. Nomme-le.',");
let src = fs.readFileSync(P, 'utf8');
const bloc = [DEBUT, ...lignes, FIN].join('\n');
src = src.includes(DEBUT)
? src.slice(0, src.indexOf(DEBUT)) + bloc + src.slice(src.indexOf(FIN) + FIN.length)
: src.replace(" 'STRATEGIES POSSIBLES :',", bloc + "\n '',\n 'STRATEGIES POSSIBLES :',");
fs.writeFileSync(P, src);
console.log(`\ncarte injectee dans ${P} (${lignes.length} lignes)`);
}
fs.mkdirSync(path.dirname(INSTANTANE), { recursive: true }); fs.mkdirSync(path.dirname(INSTANTANE), { recursive: true });
fs.writeFileSync(INSTANTANE, JSON.stringify(resultat, null, 2)); fs.writeFileSync(INSTANTANE, JSON.stringify(resultat, null, 2));