Idee du proprietaire, et elle est meilleure que la mienne : ce systeme n a pas besoin qu on lui suggere une strategie de plus, il a besoin que quelqu un VOIE que « je coupe EDF, non je remets, et hop je coupe » est absurde. Cinq automates ecrivent sur la meme installation, chacun localement correct, aucun ne voit les autres. Leurs pannes sont des CONTRADICTIONS, et elles ne se lisent que dans la SUITE des evenements — jamais dans un instantane. Le 12/09 au matin le contacteur reseau a bascule toutes les 4 minutes pendant dix minutes : une automatisation HA coupait a 35 % de SOC, l hysteresis rallumait. Aucun automate ne l a vu ; un humain oui. Sortie elargie : `constat`, `gravite` (aucune/faible/moyenne/haute) et `ce_qui_cloche`, valides strictement, publies dans trois helpers et dans le logbook. Le snapshot porte desormais l anciennete des cinq transitions cles (contacteur, verrou, takeover, etat EVCS, mode ACM). ⚠️ VALIDATION SCINDEE. Essai sur l oscillation reelle : le modele a produit un constat JUSTE et un `current_max_a` de 100, hors bornes. La validation tout-ou-rien jetait le diagnostic avec le champ fautif — c est-a-dire exactement ce qu on lui demande, perdu a cause de ce qu on ne lui demande plus. Les champs d ACTION gardent leurs bornes mais sont NEUTRALISES au lieu de faire echouer l observation ; `actionUtilisable` est le seul drapeau qui autoriserait un jour un cablage, et les compteurs ne comptent que lui. Tests advisor : +6. Deux attendus mis a la nouvelle semantique dans le meme commit (hors bornes -> observation gardee, action neutralisee). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
267 lines
16 KiB
JavaScript
267 lines
16 KiB
JavaScript
#!/usr/bin/env node
|
|
// Campagne de rejeu du conseiller IA sur des instants REELS.
|
|
// Chaque cas rejoue un moment mesure dont on connait la suite. Cinq passages
|
|
// par cas : un seul ne mesure rien, le modele n'est pas deterministe, et
|
|
// l'ECART entre passages est souvent plus parlant que la justesse.
|
|
// node tools/campagne-conseiller.mjs [passages]
|
|
import fs from 'fs';
|
|
import vm from 'vm';
|
|
|
|
const PASSAGES = Number(process.argv[2] || 5);
|
|
const URL = 'http://10.10.10.8:8000/v1/chat/completions';
|
|
const code = fs.readFileSync('src/advisor/BUILD_PROMPT.js', 'utf8');
|
|
|
|
function buildPrompt(snapshot, nowIso) {
|
|
const ctx = vm.createContext({
|
|
$input: { first: () => ({ json: { data: JSON.stringify(snapshot) } }) },
|
|
Intl, Date: class extends Date { constructor(...a) { super(...(a.length ? a : [nowIso])); } },
|
|
Math, Number, String, JSON, console,
|
|
});
|
|
return vm.runInContext(`(function(){ ${code} })()`, ctx)[0].json;
|
|
}
|
|
|
|
const BASE = {
|
|
advisor_enabled: 'on', advisor_agree_count: '0', advisor_diverge_count: '0',
|
|
advisor_prev_strategy: 'auto_solar_plus',
|
|
soc_percent: '50', battery_current_a: '0',
|
|
pv_power_w: '0', pv_power_10m_avg_w: '0', house_ac_power_w: '700',
|
|
grid_state: 'off', acin1_available: '0',
|
|
evcs_vehicle_connected: 'off', evcs_charging: 'off', evcs_current_a: '0',
|
|
evcs_surplus_state: 'vehicle_not_connected', evcs_source_mode: 'Bloqué',
|
|
user_strategy: 'auto_solar_plus',
|
|
allow_grid_hc_night: 'on', allow_grid_hc_midday: 'off',
|
|
night_target_soc_actual: '35',
|
|
current_max_auto_a: '22', plafond_edf_on_a: '20',
|
|
departure_time: '', departure_priority_enabled: 'off', required_energy_kwh: '0',
|
|
today_remaining_kwh: '0', tomorrow_kwh: '30',
|
|
balance_status: 'ok', balance_last_run: 'recent',
|
|
balance_required_soc: '40', balance_required_soc_prudent: '46',
|
|
balance_energy_to_cover_kwh: '2.0', balance_crossing_hour: '09 h',
|
|
balance_grid_needed: 'off', balance_grid_energy_needed_kwh: '0',
|
|
balance_pv_censored: 'off', balance_censor_reason: '',
|
|
balance_storage_shortfall_kwh: '6.8',
|
|
balance_next_deficit_kwh: '2.0', balance_next_deficit_window: '(cette nuit)',
|
|
balance_storm_level: 'Vert', balance_storm_reserve_soc: '38',
|
|
modules_en_service: '4',
|
|
bms_max_discharge_current_a: '100', bms_max_charge_current_a: '100',
|
|
grid_setpoint_w: '700',
|
|
mppt_operation_modes: 'off,off,off,off,off,off',
|
|
vehicle_selected: 'VW',
|
|
};
|
|
const LIM = Array(6).fill('voltage_current_limited').join(',');
|
|
const MAX = Array(6).fill('mppt_active').join(',');
|
|
|
|
export const CAS = [
|
|
// ─────────── NUIT ET RESEAU ───────────
|
|
{ id:'1-nuit-bloquee-sans-vehicule', t:'2026-09-12T04:00:00+02:00', certain:true,
|
|
titre:"12/09 04:00 — SOC 20, HC ouvertes, aucun vehicule, bilan 59,1 %",
|
|
attendu:"AGIR : engager le reseau, cible ~59 %",
|
|
s:{ soc_percent:'20', battery_current_a:'0', house_ac_power_w:'612',
|
|
grid_state:'on', acin1_available:'1', night_target_soc_actual:'59.1',
|
|
tomorrow_kwh:'75.2', balance_required_soc:'59.1', balance_required_soc_prudent:'66.2',
|
|
balance_energy_to_cover_kwh:'3.13', balance_grid_needed:'on',
|
|
balance_grid_energy_needed_kwh:'3.4', balance_storage_shortfall_kwh:'6.77' } },
|
|
|
|
{ id:'2-nuit-relachee-trop-tot', t:'2026-09-03T03:00:00+02:00', certain:true,
|
|
titre:"03/09 03:00 — SOC 45, bilan 50,4 % (releche a tort : batterie a 20 % a 06:30)",
|
|
attendu:"AGIR : tenir le reseau, cible ~50 %",
|
|
s:{ soc_percent:'45', battery_current_a:'2', house_ac_power_w:'700',
|
|
grid_state:'on', acin1_available:'1', evcs_vehicle_connected:'on',
|
|
evcs_surplus_state:'night_session_done_idle', evcs_source_mode:'Réseau HC autorisé',
|
|
night_target_soc_actual:'50.4', tomorrow_kwh:'33.9',
|
|
balance_required_soc:'50.4', balance_required_soc_prudent:'57',
|
|
balance_energy_to_cover_kwh:'4.3', balance_grid_needed:'on', balance_grid_energy_needed_kwh:'1.1' } },
|
|
|
|
{ id:'3-nuit-couverte-ne-rien-faire', t:'2026-09-12T04:00:00+02:00', certain:true,
|
|
titre:"CONTREFACTUEL — meme nuit mais SOC 70, au-dessus des 59,1 % requis",
|
|
attendu:"NE RIEN FAIRE : auto_solar_plus, pas de reseau",
|
|
s:{ soc_percent:'70', battery_current_a:'-12', house_ac_power_w:'612',
|
|
grid_state:'off', acin1_available:'0', night_target_soc_actual:'59.1',
|
|
tomorrow_kwh:'75.2', balance_required_soc:'59.1', balance_required_soc_prudent:'66.2',
|
|
balance_energy_to_cover_kwh:'3.13', balance_grid_needed:'off' } },
|
|
|
|
{ id:'4-hiver-nuit-longue', t:'2026-12-15T23:30:00+01:00', certain:false,
|
|
titre:"HIVER — 15/12 23:30, prevision 6 kWh demain, croisement tardif a 11 h",
|
|
attendu:"OUVERT : quelle cible pour une nuit d hiver ?",
|
|
s:{ soc_percent:'55', battery_current_a:'-18', house_ac_power_w:'1400',
|
|
grid_state:'off', acin1_available:'0', night_target_soc_actual:'85',
|
|
tomorrow_kwh:'6.2', balance_required_soc:'88', balance_required_soc_prudent:'97',
|
|
balance_energy_to_cover_kwh:'9.4', balance_grid_needed:'on',
|
|
balance_grid_energy_needed_kwh:'5.1', balance_crossing_hour:'11 h',
|
|
balance_storage_shortfall_kwh:'8.2' } },
|
|
|
|
// ─────────── SOLAIRE ET VOITURE ───────────
|
|
{ id:'5-soir-deficit-ne-pas-demarrer', t:'2026-09-03T19:44:00+02:00', certain:true,
|
|
titre:"03/09 19:44 — PV 758 W mais maison 1202 W : excedent NEGATIF (-444 W)",
|
|
attendu:"NE PAS DEMARRER la charge",
|
|
s:{ soc_percent:'94', battery_current_a:'-12.2', pv_power_w:'758', pv_power_10m_avg_w:'900',
|
|
house_ac_power_w:'1202', evcs_vehicle_connected:'on', vehicle_selected:'VW',
|
|
evcs_source_mode:'Potentiel solaire', mppt_operation_modes:MAX,
|
|
balance_required_soc:'99.7', balance_required_soc_prudent:'100',
|
|
balance_energy_to_cover_kwh:'11.66', balance_grid_needed:'on', tomorrow_kwh:'33.9' } },
|
|
|
|
{ id:'6-censure-demarrer', t:'2026-09-03T18:50:00+02:00', certain:true,
|
|
titre:"03/09 18:50 — PV 769 W, maison 755 W, chargeurs BRIDES : ressource refusee",
|
|
attendu:"DEMARRER : la ressource est gratuite",
|
|
s:{ soc_percent:'94', battery_current_a:'0', pv_power_w:'769', pv_power_10m_avg_w:'800',
|
|
house_ac_power_w:'755', evcs_vehicle_connected:'on', vehicle_selected:'VW',
|
|
evcs_source_mode:'Potentiel solaire', mppt_operation_modes:LIM,
|
|
balance_pv_censored:'on', balance_censor_reason:'batterie_pleine_sans_puits',
|
|
balance_required_soc:'99.7', balance_required_soc_prudent:'100',
|
|
balance_energy_to_cover_kwh:'11.66', balance_grid_needed:'on',
|
|
balance_grid_energy_needed_kwh:'4.9', tomorrow_kwh:'33.9' } },
|
|
|
|
{ id:'7-boost-evince-le-solaire', t:'2026-09-11T17:02:00+02:00', certain:true,
|
|
titre:"11/09 17:02 — BOOST 28 A, consigne 8260 W, PV effondre a 121 W",
|
|
attendu:"AGIR : reduire la demande reseau, le solaire est evince",
|
|
s:{ soc_percent:'96', battery_current_a:'0', pv_power_w:'121', pv_power_10m_avg_w:'150',
|
|
house_ac_power_w:'7695', evcs_vehicle_connected:'on', evcs_charging:'on',
|
|
evcs_current_a:'28', vehicle_selected:'JEEP', user_strategy:'force_grid_now',
|
|
grid_state:'on', acin1_available:'1', grid_setpoint_w:'8260',
|
|
evcs_source_mode:'Réseau forcé', evcs_surplus_state:'charging_hold_grid',
|
|
mppt_operation_modes:LIM, plafond_edf_on_a:'28',
|
|
balance_pv_censored:'on', balance_censor_reason:'production_bridee',
|
|
balance_required_soc:'62', balance_required_soc_prudent:'70',
|
|
balance_energy_to_cover_kwh:'5.2', balance_grid_needed:'on',
|
|
balance_grid_energy_needed_kwh:'1.4', balance_crossing_hour:'09 h',
|
|
today_remaining_kwh:'8.4', tomorrow_kwh:'75.2' } },
|
|
|
|
{ id:'8-voiture-pleine-ne-pas-reessayer', t:'2026-09-07T14:28:00+02:00', certain:true,
|
|
titre:"07/09 14:28 — borne annonce charge terminee, vehicule branche immobile",
|
|
attendu:"NE RIEN FAIRE : ne pas relancer la charge",
|
|
s:{ soc_percent:'97', battery_current_a:'1', pv_power_w:'2114', pv_power_10m_avg_w:'2000',
|
|
house_ac_power_w:'1965', evcs_vehicle_connected:'on', evcs_current_a:'0',
|
|
vehicle_selected:'JEEP', evcs_surplus_state:'day_vehicle_full_idle',
|
|
evcs_source_mode:'Bloqué', mppt_operation_modes:LIM,
|
|
balance_pv_censored:'on', balance_censor_reason:'batterie_pleine_sans_puits',
|
|
balance_required_soc:'58', balance_required_soc_prudent:'66',
|
|
balance_energy_to_cover_kwh:'4.6', balance_grid_needed:'off',
|
|
balance_crossing_hour:'09 h', today_remaining_kwh:'12', tomorrow_kwh:'30' } },
|
|
|
|
// ─────────── PROTECTION DU MATERIEL ───────────
|
|
{ id:'9-pointe-cuisson-proteger-le-parc', t:'2026-09-05T20:40:00+02:00', certain:true,
|
|
titre:"05/09 20:40 — maison 4944 W, batterie a -97,4 A pour une limite de 100 A",
|
|
attendu:"AGIR : appeler le reseau, le parc est a sa limite",
|
|
s:{ soc_percent:'79', battery_current_a:'-97.4', pv_power_w:'320', pv_power_10m_avg_w:'500',
|
|
house_ac_power_w:'4944', grid_state:'off', acin1_available:'0',
|
|
balance_required_soc:'62', balance_required_soc_prudent:'70',
|
|
balance_energy_to_cover_kwh:'5.1', balance_grid_needed:'on', tomorrow_kwh:'32' } },
|
|
|
|
{ id:'10-nuage-sur-charge-28a', t:'2026-09-11T18:00:00+02:00', certain:true,
|
|
titre:"11/09 18:00 — voiture a 28 A, consigne descendue a 1281 W, PV 6058 W",
|
|
attendu:"AGIR : garder de la marge reseau, un nuage tomberait sur la batterie",
|
|
s:{ soc_percent:'96', battery_current_a:'-0.7', pv_power_w:'6058', pv_power_10m_avg_w:'5500',
|
|
house_ac_power_w:'6845', evcs_vehicle_connected:'on', evcs_charging:'on',
|
|
evcs_current_a:'28', vehicle_selected:'JEEP', user_strategy:'force_grid_now',
|
|
grid_state:'on', acin1_available:'1', grid_setpoint_w:'1281',
|
|
evcs_source_mode:'Réseau forcé', evcs_surplus_state:'charging_hold_grid',
|
|
mppt_operation_modes:MAX, plafond_edf_on_a:'28',
|
|
balance_required_soc:'62', balance_required_soc_prudent:'70',
|
|
balance_energy_to_cover_kwh:'5.2', balance_grid_needed:'on',
|
|
balance_grid_energy_needed_kwh:'1.4', balance_crossing_hour:'09 h',
|
|
today_remaining_kwh:'4', tomorrow_kwh:'75.2' } },
|
|
|
|
// ─────────── CAS OUVERTS ───────────
|
|
{ id:'11-batterie-pleine-midi', t:'2026-09-11T12:00:00+02:00', certain:false,
|
|
titre:"11/09 12:00 — SOC 98, production refusee, AUCUN vehicule branche",
|
|
attendu:"OUVERT : y a-t-il quelque chose a faire ?",
|
|
s:{ soc_percent:'98', battery_current_a:'2.2', pv_power_w:'810', pv_power_10m_avg_w:'900',
|
|
house_ac_power_w:'702', mppt_operation_modes:LIM,
|
|
balance_pv_censored:'on', balance_censor_reason:'batterie_pleine_sans_puits',
|
|
balance_required_soc:'55', balance_required_soc_prudent:'63',
|
|
balance_energy_to_cover_kwh:'4.1', balance_grid_needed:'off',
|
|
balance_crossing_hour:'09 h', balance_next_deficit_kwh:'4.1',
|
|
balance_next_deficit_window:'(ce soir 20 h -> 09 h)',
|
|
today_remaining_kwh:'45', tomorrow_kwh:'75.2' } },
|
|
|
|
{ id:'12-orage-annonce', t:'2026-09-12T15:30:00+02:00', certain:false,
|
|
titre:"Vigilance ORANGE annoncee, SOC 72, coupures frequentes sur ce site",
|
|
attendu:"OUVERT : jusqu ou constituer la reserve ?",
|
|
s:{ soc_percent:'72', battery_current_a:'35', pv_power_w:'4200', pv_power_10m_avg_w:'4000',
|
|
house_ac_power_w:'900', mppt_operation_modes:MAX,
|
|
balance_storm_level:'Orange', balance_storm_reserve_soc:'88',
|
|
balance_required_soc:'55', balance_required_soc_prudent:'62',
|
|
balance_energy_to_cover_kwh:'3.2', balance_grid_needed:'off',
|
|
balance_crossing_hour:'09 h', today_remaining_kwh:'18', tomorrow_kwh:'40' } },
|
|
|
|
{ id:'13-voiture-empruntee-echeance', t:'2026-09-11T17:00:00+02:00', certain:false,
|
|
titre:"11/09 17:00 — voiture EMPRUNTEE a rendre a 19 h, 28 A possible, soleil present",
|
|
attendu:"OUVERT : vitesse contre solaire, arbitrage humain",
|
|
s:{ soc_percent:'96', battery_current_a:'-10', pv_power_w:'4255', pv_power_10m_avg_w:'4000',
|
|
house_ac_power_w:'4888', evcs_vehicle_connected:'on', evcs_charging:'on',
|
|
evcs_current_a:'20', vehicle_selected:'JEEP', user_strategy:'force_charge_now',
|
|
grid_state:'on', acin1_available:'1', grid_setpoint_w:'700',
|
|
evcs_source_mode:'Solaire', evcs_surplus_state:'charging_hold_manual_local',
|
|
mppt_operation_modes:MAX, plafond_edf_on_a:'28',
|
|
required_energy_kwh:'12', departure_time:'19:00', departure_priority_enabled:'on',
|
|
balance_required_soc:'62', balance_required_soc_prudent:'70',
|
|
balance_energy_to_cover_kwh:'5.2', balance_grid_needed:'on',
|
|
balance_grid_energy_needed_kwh:'1.4', balance_crossing_hour:'09 h',
|
|
today_remaining_kwh:'6', tomorrow_kwh:'75.2' } },
|
|
|
|
{ id:'4b-hiver-parc-12', t:'2026-12-15T23:30:00+01:00', certain:false,
|
|
titre:"HIVER avec le parc de cet hiver — 12 equivalents (4 US2000C + 4 US5000)",
|
|
attendu:"OUVERT : la cible baisse-t-elle quand la reserve triple ?",
|
|
s:{ soc_percent:'55', battery_current_a:'-18', house_ac_power_w:'1400',
|
|
grid_state:'off', acin1_available:'0', night_target_soc_actual:'85',
|
|
tomorrow_kwh:'6.2', balance_required_soc:'52', balance_required_soc_prudent:'59',
|
|
balance_energy_to_cover_kwh:'9.4', balance_grid_needed:'on',
|
|
balance_grid_energy_needed_kwh:'0', balance_crossing_hour:'11 h',
|
|
balance_storage_shortfall_kwh:'0',
|
|
modules_en_service:'12', bms_max_discharge_current_a:'300',
|
|
bms_max_charge_current_a:'300' } },
|
|
|
|
{ id:'4c-hiver-parc-16', t:'2026-12-15T23:30:00+01:00', certain:false,
|
|
titre:"HIVER avec 16 equivalents (vieux modules repares)",
|
|
attendu:"OUVERT : la cible baisse-t-elle encore ?",
|
|
s:{ soc_percent:'55', battery_current_a:'-18', house_ac_power_w:'1400',
|
|
grid_state:'off', acin1_available:'0', night_target_soc_actual:'85',
|
|
tomorrow_kwh:'6.2', balance_required_soc:'44', balance_required_soc_prudent:'50',
|
|
balance_energy_to_cover_kwh:'9.4', balance_grid_needed:'off',
|
|
balance_grid_energy_needed_kwh:'0', balance_crossing_hour:'11 h',
|
|
balance_storage_shortfall_kwh:'0',
|
|
modules_en_service:'16', bms_max_discharge_current_a:'400',
|
|
bms_max_charge_current_a:'400' } },
|
|
|
|
{ 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",
|
|
attendu:"Le dire dans sa raison et rester prudent",
|
|
s:{ soc_percent:'20', battery_current_a:'0', house_ac_power_w:'612',
|
|
grid_state:'on', acin1_available:'1', night_target_soc_actual:'35',
|
|
tomorrow_kwh:'75.2', balance_status:'degrade',
|
|
balance_required_soc:'unknown', balance_energy_to_cover_kwh:'unknown' } },
|
|
];
|
|
|
|
async function demander(messages) {
|
|
const r = await fetch(URL, { method:'POST', headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({ model:'qwen3-coder', temperature:0.1, max_tokens:600, messages }) });
|
|
const j = await r.json();
|
|
const c = j?.choices?.[0]?.message?.content ?? '';
|
|
const t = c.replace(/<think>[\s\S]*?<\/think>/gi,'').replace(/```(?:json)?/gi,'').trim();
|
|
const a = t.indexOf('{'); if (a === -1) return { erreur: t.slice(0,120) };
|
|
let d=0,b=-1; for (let i=a;i<t.length;i++){ if(t[i]==='{')d++; else if(t[i]==='}'){d--; if(!d){b=i;break;}} }
|
|
if (b === -1) return { erreur: 'JSON non termine' };
|
|
try { return JSON.parse(t.slice(a,b+1)); } catch(e) { return { erreur: e.message }; }
|
|
}
|
|
|
|
const resultats = [];
|
|
for (const cas of CAS) {
|
|
const p = buildPrompt({ ...BASE, ...cas.s }, cas.t);
|
|
const passages = [];
|
|
for (let i = 0; i < PASSAGES; i++) passages.push(await demander(p.messages));
|
|
resultats.push({ ...cas, passages });
|
|
const cibles = passages.map(x => x.night_target_soc).filter(Number.isFinite);
|
|
const ecart = cibles.length ? Math.max(...cibles) - Math.min(...cibles) : null;
|
|
console.log(`\n═══ ${cas.id}${cas.certain ? '' : ' [OUVERT]'}`);
|
|
console.log(` ${cas.titre}`);
|
|
console.log(` attendu : ${cas.attendu}`);
|
|
for (const x of passages) {
|
|
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)}`);
|
|
}
|
|
if (ecart !== null) console.log(` → ecart de cible entre passages : ${ecart} pts`);
|
|
}
|
|
fs.writeFileSync('docs/mesures/campagne-conseiller-' + new Date().toISOString().slice(0,10) + '.json',
|
|
JSON.stringify(resultats, null, 2));
|
|
console.log('\nJournal complet : docs/mesures/campagne-conseiller-*.json');
|