Incident 27-28/09 : l'agent presentait le surplus solaire de demain comme de l'energie batterie et lisait une projection de jour du controleur la nuit. Ce capteur ne relaie que le bilan (meme arbitre que la decision de nuit) ; bilan > 30 min -> indisponible. Tests 65/65. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uJ47NxgfpGnuuMr3f2n7Z
323 lines
20 KiB
JavaScript
323 lines
20 KiB
JavaScript
// Tests des diagnostics publies pour l'agent vocal (tools/diagnostics-agent.mjs).
|
|
// Les cas PV reprennent les gammes du banc OpenClaw (HANDOFF §77) : G3, G4, G5, G7.
|
|
// node tests/diagnostics-agent/run.mjs
|
|
import fs from 'node:fs';
|
|
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, COMPTEURS_IMPORT, COMPTEUR_EDF, MPPT_MODES, ORIGINE_PAR_SOURCE, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit } from '../../tools/diagnostics-agent.mjs';
|
|
|
|
const LIM = 'voltage_current_limited', ACT = 'mppt_active';
|
|
function st({ pv, conso, reseau, soc = 80, courant = 0, edf = 'off', modes }) {
|
|
const m = new Map();
|
|
const put = (id, v) => m.set(id, { entity_id: id, state: v === null ? 'unavailable' : String(v) });
|
|
put('sensor.victron_pv_power_system', pv);
|
|
put('sensor.victron_consumption_power_l1', conso);
|
|
put('sensor.victron_grid_power_l1', reseau);
|
|
put('sensor.victron_battery_soc', soc);
|
|
put('sensor.victron_battery_current', courant);
|
|
put('switch.edf_general_ok_switch', edf);
|
|
const liste = Array.isArray(modes) ? modes : MPPT_MODES.map(() => modes);
|
|
MPPT_MODES.forEach((id, i) => put(id, liste[i]));
|
|
return m;
|
|
}
|
|
|
|
const cas = [
|
|
['G3 batterie pleine, 7 brides, import nul -> bridage normal',
|
|
st({ pv: 1570, conso: 1500, reseau: 0, soc: 97, courant: 1, modes: LIM }), 'bridage_normal'],
|
|
['G4 vraie baisse : 7 a fond, SOC 69, +25 A -> ressource limitee',
|
|
st({ pv: 1600, conso: 400, reseau: 0, soc: 69, courant: 25, modes: ACT }), 'ressource_limitee'],
|
|
['G5 brides MAIS import ~ conso -> substitution',
|
|
st({ pv: 300, conso: 1200, reseau: 1150, soc: 97, courant: 0, edf: 'on', modes: LIM }), 'substitution_edf'],
|
|
['G7 un seul bride, import nul -> bridage normal (ressource dispo)',
|
|
st({ pv: 5200, conso: 2000, reseau: 0, soc: 92, courant: 60, modes: [LIM, ACT, ACT, ACT, ACT, ACT, ACT] }), 'bridage_normal'],
|
|
['bruit du compteur (60 W) contacteur ferme : pas une substitution',
|
|
st({ pv: 1500, conso: 1400, reseau: 60, soc: 96, edf: 'on', modes: LIM }), 'bridage_normal'],
|
|
['un seul bride suffit a signer la substitution si le reseau importe',
|
|
st({ pv: 3000, conso: 3500, reseau: 900, edf: 'on', modes: [LIM, ACT, ACT, ACT, ACT, ACT, ACT] }), 'substitution_edf'],
|
|
['tous a fond + import -> reseau en appoint, pas une anomalie',
|
|
st({ pv: 2000, conso: 3500, reseau: 1500, edf: 'on', modes: ACT }), 'reseau_en_appoint'],
|
|
['nuit : PV 0, tous off -> pas de production',
|
|
st({ pv: 0, conso: 600, reseau: 0, modes: 'off' }), 'pas_de_production'],
|
|
['nuit + reseau ferme : toujours pas de substitution',
|
|
st({ pv: 0, conso: 600, reseau: 600, edf: 'on', modes: 'off' }), 'pas_de_production'],
|
|
['reseau indisponible -> indetermine (ne pas conclure)',
|
|
st({ pv: 1500, conso: 1400, reseau: null, modes: LIM }), 'indetermine'],
|
|
['modes illisibles en journee -> indetermine',
|
|
st({ pv: 1500, conso: 1400, reseau: 0, modes: 'unavailable' }), 'indetermine'],
|
|
];
|
|
|
|
let ko = 0;
|
|
for (const [nom, etats, attendu] of cas) {
|
|
const r = diagnosticPv(etats);
|
|
const ok = r.etat === attendu;
|
|
if (!ok) ko++;
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}${ok ? '' : ` (obtenu ${r.etat})`}`);
|
|
}
|
|
|
|
const cel = [
|
|
['releve frais -> ecart et module publies', { spread_max: 21, weakest: 'K220803C30160323', age_min: 1 },
|
|
(r) => r.etat === 21 && r.attributs.module_le_plus_sensible === 'K220803C30160323'],
|
|
['releve a 30 min (limite) -> encore frais', { spread_max: 5, weakest: 'Y1', age_min: 30 }, (r) => r.etat === 5],
|
|
['G13b releve a 42 min -> perime, AUCUNE valeur visible', { spread_max: 21, weakest: 'K220803C30160323', age_min: 42 },
|
|
(r) => r.etat === 'perime' && !('module_le_plus_sensible' in r.attributs)
|
|
&& !JSON.stringify(r).includes('21,') && !JSON.stringify(r).includes('K2208')],
|
|
['table vide -> muet', { spread_max: null, weakest: null, age_min: null }, (r) => r.etat === 'muet'],
|
|
];
|
|
for (const [nom, row, test] of cel) {
|
|
const ok = test(etatCellules(row));
|
|
if (!ok) ko++;
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
|
}
|
|
// --- Production du jour (G1, G9) ---------------------------------------
|
|
const MIDI = new Date('2026-09-24T12:00:00Z'); // 14:00 a Paris
|
|
const NUIT = new Date('2026-09-24T22:40:00Z'); // 00:40 a Paris le 25
|
|
function rend(v1, v2, v3, change = '2026-09-24T11:59:00Z') {
|
|
const m = new Map();
|
|
[['sensor.airtria_mppt1_yield_today', v1], ['sensor.mppt_450_200_hq2449yr9pt_yield_today', v2],
|
|
['sensor.v1_yield_today', v3]].forEach(([id, v]) => {
|
|
const [val, lc] = Array.isArray(v) ? v : [v, change];
|
|
m.set(id, { state: val === null ? 'unavailable' : String(val), last_changed: lc });
|
|
});
|
|
// un sous-tracker present ne doit JAMAIS etre additionne
|
|
m.set('sensor.mppt_450_200_hq2449yr9pt_tracker_solar_1_yield_today', { state: '20.0', last_changed: change });
|
|
return m;
|
|
}
|
|
const prod = [
|
|
['G1 somme des trois (45,1 du 24/09), sous-tracker ignore', rend(9.45, 32.71, 2.97), MIDI,
|
|
(r) => r.etat === 45.1 && r.attributs.statut === 'complet'],
|
|
['G9 un chargeur unavailable -> partiel, somme du reste, muet nomme', rend(9.45, 32.71, null), MIDI,
|
|
(r) => r.etat === 42.2 && r.attributs.statut === 'partiel' && r.attributs.chargeurs_muets.join() === 'v1'],
|
|
['tous muets -> indisponible', rend(null, null, null), MIDI, (r) => r.etat === 'indisponible'],
|
|
['00:40 : un chargeur pas encore remis a zero (valeur d hier) -> compte 0',
|
|
rend(['0.0', '2026-09-24T22:37:00Z'], ['32.71', '2026-09-24T17:30:00Z'], ['0.0', '2026-09-24T22:37:00Z']), NUIT,
|
|
(r) => r.etat === 0 && r.attributs.pas_encore_remis_a_zero.join() === 'mppt_450_200'],
|
|
['heure d ete : 23:30 Paris le 24 = encore aujourd hui', rend(9.45, 32.71, 2.97, '2026-09-24T17:30:00Z'),
|
|
new Date('2026-09-24T21:30:00Z'), (r) => r.etat === 45.1],
|
|
];
|
|
for (const [nom, etats, t, test] of prod) {
|
|
const ok = test(productionJour(etats, t));
|
|
if (!ok) { ko++; console.log(JSON.stringify(productionJour(etats, t))); }
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
|
}
|
|
|
|
// --- Charge voiture : RELAIS du controleur (G6, G7) --------------------
|
|
function borne({ branche = 'on', p = 0, source = 'Solaire', raison = 'Excedent negatif | Profil=x', pv = 2000, conso = 3854, modes = ACT }) {
|
|
const m = st({ pv, conso, reseau: 0, modes });
|
|
const put = (id, v) => m.set(id, { state: String(v) });
|
|
put('binary_sensor.victron_evcs_vehicle_connected', branche);
|
|
put('sensor.victron_evcs_ac_power', p);
|
|
put('input_text.victron_evcs_source_mode', source);
|
|
put('input_text.victron_evcs_surplus_reason', raison);
|
|
put('input_select.victron_evcs_strategy', 'auto_solar_plus');
|
|
return m;
|
|
}
|
|
const charge = [
|
|
['G6 branchee, pas en charge -> en_attente + raison du controleur, excedent explicatif',
|
|
borne({ source: 'Bloqué', raison: 'Excedent insuffisant | Profil=outside_window' }),
|
|
(r) => r.etat === 'en_attente' && r.attributs.raison_controleur === 'Excedent insuffisant' && r.attributs.excedent_w === -1854],
|
|
['en charge sur source solaire -> en_charge_solaire', borne({ p: 3600, source: 'Solaire' }), (r) => r.etat === 'en_charge_solaire'],
|
|
['en charge source reseau -> en_charge_reseau', borne({ p: 3600, source: 'Réseau HC autorisé' }), (r) => r.etat === 'en_charge_reseau'],
|
|
// Incident du 25/09 01:25-06:00 : JEEP sur batterie la nuit, publie « solaire ».
|
|
['INCIDENT 25/09 : assistance batterie nuit -> en_charge_batterie, JAMAIS solaire',
|
|
borne({ p: 1198, source: 'Assistance batterie nuit', pv: 0, conso: 900, modes: 'off' }),
|
|
(r) => r.etat === 'en_charge_batterie' && !/solaire/i.test(r.attributs.explication.split('(')[0])],
|
|
['sonde hybride -> en_charge_sonde', borne({ p: 2300, source: 'Probe hybride' }), (r) => r.etat === 'en_charge_sonde'],
|
|
['potentiel solaire -> en_charge_solaire', borne({ p: 2300, source: 'Potentiel solaire' }), (r) => r.etat === 'en_charge_solaire'],
|
|
['reseau force -> en_charge_reseau', borne({ p: 3600, source: 'Réseau forcé' }), (r) => r.etat === 'en_charge_reseau'],
|
|
['libelle inconnu -> en_charge sans origine devinee', borne({ p: 3600, source: 'nouveau_mode' }), (r) => r.etat === 'en_charge'],
|
|
['non branchee -> non_branchee', borne({ branche: 'off' }), (r) => r.etat === 'non_branchee'],
|
|
['G7 excedent positif et bride, mais controleur bloque -> en_attente (on RELAIE, on ne rejuge pas)',
|
|
borne({ pv: 5000, conso: 1500, modes: LIM, source: 'Bloqué', raison: 'Vehicule plein' }),
|
|
(r) => r.etat === 'en_attente' && r.attributs.diagnostic_pv === 'bridage_normal'],
|
|
['borne illisible -> indetermine', (() => { const m = borne({}); m.set('sensor.victron_evcs_ac_power', { state: 'unavailable' }); return m; })(),
|
|
(r) => r.etat === 'indetermine'],
|
|
];
|
|
for (const [nom, etats, test] of charge) {
|
|
const ok = test(chargeSolaire(etats));
|
|
if (!ok) { ko++; console.log(JSON.stringify(chargeSolaire(etats))); }
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
|
}
|
|
|
|
// Chaque libelle publie par le controleur doit avoir une origine explicite :
|
|
// lu dans SON code, pour qu'un libelle ajoute la-bas fasse tomber ce test.
|
|
const srcCtrl = fs.readFileSync(new URL('../../src/nodes/HA_SERVICE_CALLS_OUTPUT.js', import.meta.url), 'utf8');
|
|
const bloc = srcCtrl.slice(srcCtrl.indexOf('const sourceModeLabel ='), srcCtrl.indexOf('context.sourceMode;', srcCtrl.indexOf('const sourceModeLabel =')));
|
|
const libelles = [...bloc.matchAll(/\? '([^']+)'/g)].map((m) => m[1]).filter((l) => l !== 'Bloqué');
|
|
const nonCouverts = libelles.filter((l) => !ORIGINE_PAR_SOURCE[l]);
|
|
const okLib = libelles.length >= 6 && nonCouverts.length === 0;
|
|
if (!okLib) ko++;
|
|
console.log(`${okLib ? 'OK ' : 'KO '} les ${libelles.length} libelles de source du controleur ont une origine${okLib ? '' : ` (manquent : ${nonCouverts.join(', ') || 'table introuvable'})`}`);
|
|
|
|
// --- Bilan de la nuit : la nuit reelle du 24 au 25/09 (incident de 11:49) ---
|
|
const nuit = {
|
|
debut: new Date('2026-09-24T16:00:00Z'), fin: new Date('2026-09-25T08:00:00Z'),
|
|
dateLue: 'vendredi 25 septembre',
|
|
fermetures: [{ ts: new Date('2026-09-25T04:01:00Z') }],
|
|
e: { importe: 2.71, voiture: 7.96 },
|
|
b: { depart: 97, bas: 31, heure_bas: new Date('2026-09-25T07:16:00Z'), maintenant: 32 },
|
|
sessions: [{ debut: new Date('2026-09-24T23:25:00Z'), fin: new Date('2026-09-25T04:42:00Z'), terminee: true,
|
|
vehicule: 'JEEP', energie_kwh: 7.96, energie_compteur_kwh: 7.9, energie_reseau_kwh: 2.2, source_mode: 'Assistance batterie nuit' }],
|
|
};
|
|
const bn = [
|
|
['INCIDENT 11:49 : la session terminee est bien visible, 7,9 kWh dont 2,2 reseau, 01:25 -> 06:42',
|
|
bilanNuit(nuit, 'x', new Date('2026-09-25T09:49:00Z')),
|
|
(r) => r.etat === 'complet' && r.attributs.vehicules[0].energie_kwh === 7.9 && r.attributs.vehicules[0].dont_reseau_kwh === 2.2
|
|
&& r.attributs.vehicules[0].debut === '01:25' && r.attributs.vehicules[0].fin === '06:42'],
|
|
['aucun SOC voiture publie ; le 35 % n existe nulle part', bilanNuit(nuit, 'x', new Date('2026-09-25T09:49:00Z')),
|
|
(r) => !JSON.stringify(r).includes('35') && /aucun capteur/.test(r.attributs.soc_voiture)],
|
|
['reseau : fermeture 06:01, import 2,7', bilanNuit(nuit, 'x', new Date('2026-09-25T09:49:00Z')),
|
|
(r) => r.attributs.reseau_fermetures.join() === '06:01' && r.attributs.reseau_importe_kwh === 2.7],
|
|
['avant 10:00 -> en_cours, session ouverte sans heure de fin', bilanNuit({ ...nuit,
|
|
sessions: [{ ...nuit.sessions[0], terminee: false }] }, 'x', new Date('2026-09-25T03:00:00Z')),
|
|
(r) => r.etat === 'en_cours' && r.attributs.vehicules[0].fin === null],
|
|
['nuit sans charge -> liste vide', bilanNuit({ ...nuit, sessions: [] }, 'x', new Date('2026-09-25T09:49:00Z')),
|
|
(r) => r.attributs.vehicules.length === 0],
|
|
];
|
|
for (const [nom, r, test] of bn) {
|
|
const ok = test(r);
|
|
if (!ok) { ko++; console.log(JSON.stringify(r)); }
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
|
}
|
|
|
|
// Raccordement reseau. Cas de reference : 26/09 11:38, contacteur ouvert a 07:24.
|
|
const T = new Date('2026-09-26T09:38:00Z');
|
|
function rs({ sw = 'off', swA = '2026-09-26T05:24:03Z', ac = 0, gp = 0, gpA = '2026-09-26T05:24:05Z', settle }) {
|
|
const m = new Map();
|
|
const put = (id, v, at) => m.set(id, { entity_id: id, state: v === null ? 'unavailable' : String(v), last_changed: at });
|
|
put('switch.edf_general_ok_switch', sw, swA);
|
|
put('sensor.victron_ac_in1_available', ac, swA);
|
|
put('sensor.airtria_mpii_v1_active_ac_input', ac ? 'ac_input_1' : 'not_connected', swA);
|
|
put('sensor.victron_grid_power_l1', gp, gpA);
|
|
if (settle !== undefined) put('input_number.evcs_grid_settle_seconds', settle, swA);
|
|
return m;
|
|
}
|
|
const res = [
|
|
['INCIDENT 26/09 : contacteur ouvert a 07:24, 0 W inchange depuis 4 h -> non_raccorde, PAS muet',
|
|
etatReseau(rs({}), T), (r) => r.etat === 'non_raccorde' && r.attributs.import_w === 0 && r.attributs.depuis_heure === '07:24'],
|
|
['contacteur ferme, reseau vu, mesure fraiche -> raccorde avec import',
|
|
etatReseau(rs({ sw: 'on', swA: '2026-09-26T09:00:00Z', ac: 1, gp: 850, gpA: '2026-09-26T09:37:50Z' }), T),
|
|
(r) => r.etat === 'raccorde' && r.attributs.import_w === 850],
|
|
['contacteur ferme depuis 38 min, MultiPlus ne voit pas le reseau -> mesure_muette',
|
|
etatReseau(rs({ sw: 'on', swA: '2026-09-26T09:00:00Z', ac: 0 }), T),
|
|
(r) => r.etat === 'mesure_muette' && r.attributs.import_w === null],
|
|
['contacteur ferme, reseau vu, puissance inchangee depuis 30 min -> mesure_muette',
|
|
etatReseau(rs({ sw: 'on', swA: '2026-09-26T09:00:00Z', ac: 1, gp: 0, gpA: '2026-09-26T09:08:00Z' }), T),
|
|
(r) => r.etat === 'mesure_muette'],
|
|
['contacteur ferme, puissance unavailable -> mesure_muette',
|
|
etatReseau(rs({ sw: 'on', swA: '2026-09-26T09:00:00Z', ac: 1, gp: null }), T), (r) => r.etat === 'mesure_muette'],
|
|
['fermeture il y a 60 s, ac_in pas encore la -> etablissement, pas muet',
|
|
etatReseau(rs({ sw: 'on', swA: '2026-09-26T09:37:00Z', ac: 0 }), T), (r) => r.etat === 'etablissement'],
|
|
['helper d etablissement lu : 30 s -> au-dela, ac_in 0 = muet',
|
|
etatReseau(rs({ sw: 'on', swA: '2026-09-26T09:37:00Z', ac: 0, settle: 30 }), T), (r) => r.etat === 'mesure_muette'],
|
|
['fermeture il y a 5 min, puissance inchangee depuis : pas encore figee',
|
|
etatReseau(rs({ sw: 'on', swA: '2026-09-26T09:33:00Z', ac: 1, gp: 40, gpA: '2026-09-26T09:34:00Z' }), T),
|
|
(r) => r.etat === 'raccorde'],
|
|
['contacteur ouvert mais reseau vu -> incoherent',
|
|
etatReseau(rs({ ac: 1 }), T), (r) => r.etat === 'incoherent'],
|
|
['contacteur ouvert mais import 900 W -> incoherent',
|
|
etatReseau(rs({ gp: 900, gpA: '2026-09-26T09:37:00Z' }), T), (r) => r.etat === 'incoherent'],
|
|
['contacteur unavailable -> indetermine',
|
|
etatReseau(rs({ sw: null }), T), (r) => r.etat === 'indetermine'],
|
|
];
|
|
for (const [nom, r, test] of res) {
|
|
const ok = test(r);
|
|
if (!ok) { ko++; console.log(JSON.stringify(r)); }
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
|
}
|
|
|
|
// Import depuis minuit. Releve reel du 26/09 12:19 : EDF 0,8 (= VRM), MultiPlus 0,6 + 0,1.
|
|
const [OUT, INV] = COMPTEURS_IMPORT;
|
|
const mn = { [OUT]: 1200.0, [INV]: 845.0, [COMPTEUR_EDF]: 3082.0 };
|
|
const ok26 = { [OUT]: 1200.6, [INV]: 845.1, [COMPTEUR_EDF]: 3082.83 };
|
|
const imp = [
|
|
['26/09 : le compteur EDF fait foi -> 0,8 comme VRM, MultiPlus 0,7 en recoupement',
|
|
importJour(ok26, mn), (r) => r.etat === 0.8 && r.attributs.source === 'compteur_edf' && r.attributs.victron_kwh === 0.7],
|
|
['journee sans reseau -> 0, pas indisponible',
|
|
importJour({ [OUT]: 1200, [INV]: 845, [COMPTEUR_EDF]: 3082 }, mn), (r) => r.etat === 0 && r.attributs.statut === 'complet'],
|
|
['compteur EDF remis a zero -> repli MultiPlus, jamais un import negatif',
|
|
importJour({ ...ok26, [COMPTEUR_EDF]: 0.4 }, mn), (r) => r.etat === 0.7 && r.attributs.statut === 'repli_victron'],
|
|
['compteur EDF muet -> repli MultiPlus, dit « environ »',
|
|
importJour({ ...ok26, [COMPTEUR_EDF]: null }, mn), (r) => r.etat === 0.7 && /Environ/.test(r.attributs.explication)],
|
|
['EDF muet ET un MultiPlus remis a zero -> indisponible',
|
|
importJour({ [OUT]: 3.2, [INV]: 845.1, [COMPTEUR_EDF]: null }, mn), (r) => r.etat === 'indisponible'],
|
|
['EDF muet ET un MultiPlus muet -> indisponible, pas de total partiel',
|
|
importJour({ [OUT]: 1200.6, [INV]: null, [COMPTEUR_EDF]: null }, mn), (r) => r.etat === 'indisponible'],
|
|
['valeurs de minuit inconnues -> indisponible',
|
|
importJour(ok26, {}), (r) => r.etat === 'indisponible'],
|
|
];
|
|
for (const [nom, r, test] of imp) {
|
|
const ok = test(r);
|
|
if (!ok) { ko++; console.log(JSON.stringify(r)); }
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
|
}
|
|
|
|
// --- Consommation du jour : releve reel du 26/09 a 17:11 (VRM ~25 kWh) ------
|
|
{
|
|
const [R, O] = COMPTEURS_CONSO.map(([, id]) => id);
|
|
const mi = { [R]: 9, [O]: 292.4 };
|
|
const cj = [
|
|
['conso reelle 26/09 17:11 : 0,6 + 24,6 = 25,2 kWh, complet', consoJour({ [R]: 9.6, [O]: 317 }, mi),
|
|
(r) => r.etat === 25.2 && r.attributs.statut === 'complet' && r.attributs.detail_kwh.depuis_reseau === 0.6],
|
|
['compteur remis a zero -> indisponible, jamais negatif', consoJour({ [R]: 9.6, [O]: 3.1 }, mi),
|
|
(r) => r.etat === 'indisponible'],
|
|
['un compteur muet -> partiel « au moins »', consoJour({ [R]: null, [O]: 317 }, mi),
|
|
(r) => r.etat === 24.6 && r.attributs.statut === 'partiel' && /au moins/.test(r.attributs.explication)],
|
|
['minuit inconnu -> indisponible', consoJour({ [R]: 9.6, [O]: 317 }, {}), (r) => r.etat === 'indisponible'],
|
|
];
|
|
for (const [nom, r, test] of cj) {
|
|
const ok = test(r); if (!ok) { ko++; console.log(JSON.stringify(r)); }
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
|
}
|
|
globalThis.__nConso = cj.length;
|
|
}
|
|
|
|
// --- Projection du soir : indetermine tant que la prevision est fausse -----
|
|
{
|
|
const m = new Map([['sensor.victron_battery_soc', { state: '42' }],
|
|
['sensor.energy_production_today_remaining', { state: '6.8' }], ['sensor.evcs_prevision_contre_reel', { state: '63' }]]);
|
|
const r = projectionSoir(m);
|
|
const pj = [
|
|
['projection : etat indetermine, JAMAIS un SOC chiffre (incident 27/09 « vers 90 % »)', r,
|
|
(x) => x.etat === 'indetermine' && !/\b(9\d|[1-8]\d) ?% au coucher/.test(x.attributs.explication)],
|
|
['projection : l explication donne le SOC actuel et l ecart de prevision', r,
|
|
(x) => /42 %/.test(x.attributs.explication) && /63 %/.test(x.attributs.explication)],
|
|
];
|
|
for (const [nom, x, test] of pj) {
|
|
const ok = test(x); if (!ok) { ko++; console.log(JSON.stringify(x)); }
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
|
}
|
|
globalThis.__nConso = (globalThis.__nConso || 0) + pj.length;
|
|
}
|
|
|
|
// --- Prevision de la nuit : releve reel du 28/09 01:25 ----------------------
|
|
{
|
|
const base = () => new Map(Object.entries({
|
|
'input_text.evcs_balance_status': 'ok', 'input_text.evcs_balance_last_run': '28/09 01:25',
|
|
'input_number.evcs_balance_energy_to_cover_kwh': '5.47', 'input_number.evcs_balance_grid_energy_needed_kwh': '5.77',
|
|
'input_boolean.evcs_balance_grid_needed': 'on', 'input_text.evcs_balance_crossing_hour': '09 h',
|
|
'input_number.evcs_balance_required_soc': '40.2', 'sensor.victron_battery_soc': '21',
|
|
'sensor.evcs_reseau': 'raccorde', 'input_boolean.victron_evcs_night_grid_session_committed': 'on',
|
|
'binary_sensor.victron_evcs_vehicle_connected': 'off',
|
|
// pieges : valeurs internes qui ne doivent JAMAIS apparaitre
|
|
'input_number.victron_evcs_projected_end_soc_after_charge': '73.7', 'input_number.evcs_balance_vehicle_surplus_kwh': '26.95',
|
|
}).map(([k, v]) => [k, { state: v }]));
|
|
const t = new Date('2026-09-27T23:30:00Z'); // 01:30 a Paris
|
|
const pn = [
|
|
['nuit 28/09 : 5,77 kWh EDF, 5,47 a couvrir, retour 09 h, aucune voiture', previsionNuit(base(), t),
|
|
(r) => r.etat === 5.77 && r.attributs.voiture === 'aucune' && /5.77 kWh a prendre sur EDF/.test(r.attributs.explication)],
|
|
['jamais 73,7 ni 26,95 dans la prevision (incident 27/09)', previsionNuit(base(), t),
|
|
(r) => !/73\.7|26\.95/.test(JSON.stringify(r))],
|
|
['bilan perime (> 30 min) -> indisponible', previsionNuit(base(), new Date('2026-09-28T00:30:00Z')),
|
|
(r) => r.etat === 'indisponible'],
|
|
['reseau non necessaire -> 0 et « la batterie suffit »', (() => { const m = base(); m.set('input_boolean.evcs_balance_grid_needed', { state: 'off' }); return previsionNuit(m, t); })(),
|
|
(r) => r.etat === 0 && /batterie suffit/.test(r.attributs.explication)],
|
|
];
|
|
for (const [nom, r, test] of pn) {
|
|
const ok = test(r); if (!ok) { ko++; console.log(JSON.stringify(r)); }
|
|
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
|
}
|
|
globalThis.__nConso = (globalThis.__nConso || 0) + pn.length;
|
|
}
|
|
|
|
const n = (globalThis.__nConso || 0) + cas.length + cel.length + prod.length + charge.length + 1 + bn.length + res.length + imp.length;
|
|
console.log(`\n${n - ko}/${n}`);
|
|
process.exit(ko ? 1 : 0);
|