Files
OPENCLAW_ARCHIVE3_17/workspace/tests-pv/campagne-pv.mjs
Thierry Tallagrand f121f0a196 workspace: rapport solaire, prevision de nuit, concierge, courrier, fiche allegee
- Fiche agent maison (AGENTS.md) : prevision de nuit relayee par script, import
  EDF, regle JEEP midi, saisie vehicule/niveau/depart, concierge qui relaie
  « ✅ Noté » ou « Rien n'est noté », groupe A deplace (28 649 -> 25 831 car.).
- Nouvelles references : reference-concierge.md, reference-workspace.md.
- Scripts : rapport-solaire.js, prevision-nuit.js, airtria-ha-vehicule.js,
  concierge-live.js (answer ne sort plus jamais vide, mode CONCIERGE_SIMULATION).
- Courrier Orange / AIRTRIA / Gmail : audits, regles, menage, recents.
- Bancs EVCS tests-pv (G5 accepte « anomal »).
- .gitignore : liste blanche etendue aux nouveaux scripts et bancs.
2026-09-30 11:32:44 +02:00

487 lines
27 KiB
JavaScript

#!/usr/bin/env node
// campagne-pv.mjs — Banc de test « gammes PV » pour l'agent vocal OpenClaw.
// Modèle : tools/campagne-conseiller.mjs (EVCS). Snapshot figé → prompt → Spark →
// notation sur N passages (>=3, cf. HANDOFF §63 : un seul run de 5 ne distingue
// pas 3/5 de 5/5). Ne teste PAS l'agent en direct sur Home Assistant : il injecte
// les valeurs de capteurs dans le prompt, donc reproductible et sans attendre
// qu'il fasse nuageux à midi.
//
// La fiche « Repères énergie » testée est EXTRAITE d'AGENTS.md à l'exécution :
// le banc valide donc la vraie fiche déployée, pas une copie.
//
// Snapshots + attendus = vérité-terrain fournie par EVCS (PostgreSQL/InfluxDB).
// Ne JAMAIS inventer un attendu : cf. le bug des 4,2 kWh.
//
// Usage : node campagne-pv.mjs [passages=3] [--only=<id>]
'use strict';
import fs from 'node:fs';
const PASSAGES = Number((process.argv.find(a => /^\d+$/.test(a))) || 3);
const ONLY = (process.argv.find(a => a.startsWith('--only=')) || '').slice(7);
const URL = process.env.SPARK_URL || 'http://10.10.10.8:8000/v1/chat/completions';
const AGENTS = process.env.AGENTS_MD || '/mnt/ARCH3/docker/openclaw-data/workspace/AGENTS.md';
// --- fiche "Repères énergie" extraite d'AGENTS.md (ce que l'agent vocal utilise) ---
const agents = fs.readFileSync(AGENTS, 'utf8');
const iFiche = agents.indexOf('## Repères énergie');
if (iFiche < 0) { console.error('Section "Repères énergie" introuvable dans AGENTS.md'); process.exit(2); }
let jFiche = agents.indexOf('\n## ', iFiche + 1);
if (jFiche < 0) jFiche = agents.length;
const FICHE = agents.slice(iFiche, jFiche);
const SYSTEME =
`Tu es l'assistant vocal de la maison de Thierry. On te donne l'état courant des
capteurs Home Assistant, puis une question. Réponds en UNE phrase, naturelle à
dire à voix haute, en te fondant UNIQUEMENT sur les capteurs fournis et sur ces
repères. Ne lance aucune commande, n'invente aucune valeur.
${FICHE}`;
const snapTexte = o => Object.entries(o).map(([k, v]) => `${k} = ${v}`).join('\n');
// --- GAMMES ---
// motsCles : liste de GROUPES ; chaque groupe doit avoir AU MOINS un terme présent (ET de OU).
// interdits : si l'un apparaît, la réponse est fausse (piège non évité).
// helper : les 7 capteurs de mode MPPT, tous à la même valeur
const modes = v => ({
'sensor.v1_mppt_operation_mode': v,
'sensor.airtria_mppt1_mppt_operation_mode': v,
'sensor.mppt_450_200_hq2449yr9pt_mppt_operation_mode': v,
'sensor.mppt_450_200_hq2449yr9pt_pv_tracker_solar_1_operation_mode': v,
'sensor.mppt_450_200_hq2449yr9pt_pv_tracker_solar_2_operation_mode': v,
'sensor.mppt_450_200_hq2449yr9pt_pv_tracker_solar_3_operation_mode': v,
'sensor.mppt_450_200_hq2449yr9pt_pv_tracker_solar_4_operation_mode': v,
});
const S_G3 = { // bridage midi 24/09 16:55 — réutilisé par G7
'sensor.victron_pv_power_system': '1346', 'sensor.victron_consumption_power_l1': '1170',
'sensor.victron_battery_soc': '97', 'sensor.victron_battery_current': '1',
'sensor.victron_grid_power_l1': '0', 'sensor.evcs_ac_power': '0',
'sensor.energy_production_today': '31.13', ...modes('voltage_current_limited'),
};
const CAS = [
{ // G1 — production du jour → sensor.evcs_production_jour_kwh (complet)
id: 'G1-production-du-jour',
question: "Quelle est la production solaire aujourd'hui ?",
s: {
'sensor.evcs_production_jour_kwh': '45.1',
'sensor.evcs_production_jour_kwh.statut': 'complet',
'sensor.evcs_production_jour_kwh.explication': 'Production reelle du jour : 45.1 kWh (somme des trois chargeurs).',
},
motsCles: [['45']],
interdits: ['30,2', '30.2'],
attendu: 'état 45.1 statut complet → « Production du jour = 45,1 kWh ».',
},
{ // G3 — bridage batterie pleine → LIT le diagnostic pré-calculé, ne rejuge pas
id: 'G3-bridage-batterie-pleine',
question: "Il fait plein soleil mais la production ne dépasse pas 1,3 kW. C'est une mauvaise journée solaire ?",
s: {
'sensor.evcs_pv_diagnostic': 'bridage_normal',
'sensor.evcs_pv_diagnostic.explication': '7 chargeur(s) se retiennent et le reseau n\'importe rien : bridage normal d\'une installation sans injection. De la ressource solaire reste disponible pour une charge de plus.',
'sensor.evcs_pv_diagnostic.part_reseau_pct': '0',
'sensor.evcs_pv_diagnostic.pv_w': '1346', 'sensor.evcs_pv_diagnostic.conso_w': '1170',
'sensor.evcs_pv_diagnostic.import_w': '0', 'sensor.evcs_pv_diagnostic.soc_pct': '97',
'sensor.evcs_pv_diagnostic.mppt_bridés': '7',
},
motsCles: [['brid', 'normal', 'pleine', 'preneur']],
interdits: ['mauvaise journ', 'faible ensoleill', 'peu de soleil', 'moins de soleil', 'nuage'],
attendu: 'état bridage_normal → relaie : pas une mauvaise journée. ÉCHEC si « mauvaise journée ».',
},
{ // G4 — vraie baisse → diagnostic ressource_limitee
id: 'G4-vraie-baisse',
question: "La production n'est qu'à 1,2 kW ce matin. Est-ce que les panneaux sont bridés ?",
s: {
'sensor.evcs_pv_diagnostic': 'ressource_limitee',
'sensor.evcs_pv_diagnostic.explication': 'Tous les chargeurs donnent leur maximum : la production est limitee par le soleil, rien n\'est gache.',
'sensor.evcs_pv_diagnostic.part_reseau_pct': '0',
'sensor.evcs_pv_diagnostic.pv_w': '1248', 'sensor.evcs_pv_diagnostic.conso_w': '402',
'sensor.evcs_pv_diagnostic.soc_pct': '69', 'sensor.evcs_pv_diagnostic.import_w': '0',
'sensor.evcs_pv_diagnostic.mppt_a_fond': '7',
},
motsCles: [['non', 'pas brid', 'ne sont pas', 'ressource', 'limit', 'a fond', 'mppt_active']],
interdits: [],
attendu: 'état ressource_limitee → non, pas de bridage, ressource réellement faible.',
},
{ // G6 — peut-on charger → RELAIE sensor.evcs_charge_solaire (raison contrôleur RÉELLE)
id: 'G6-charge-voiture-relais',
statut: 'RÉEL (live 01:19, raison contrôleur réelle)',
question: 'Peut-on charger la voiture sur le solaire maintenant ?',
s: {
'sensor.evcs_charge_solaire': 'en_attente',
'sensor.evcs_charge_solaire.explication': 'Voiture branchee mais pas en charge. Le controleur dit : Hors fenetre preferee sans source suffisante (source : Bloque).',
'sensor.evcs_charge_solaire.raison_controleur': 'Hors fenetre preferee sans source suffisante',
'sensor.evcs_charge_solaire.etat_controleur': 'blocked_outside_window',
'sensor.evcs_charge_solaire.excedent_w': '-915',
},
motsCles: [['controleur', 'fenetre', 'attente', 'pas en charge', 'branche']],
interdits: [],
attendu: 'relaie en_attente + raison du contrôleur (hors fenêtre). ÉCHEC si conclut « non car excédent négatif » de sa propre analyse.',
},
{ // Gnuit — assistance batterie nuit → NE JAMAIS dire solaire (bug capteur corrigé EVCS beed802)
id: 'Gnuit-charge-batterie-pas-solaire',
statut: 'RÉEL (état exact EVCS après correctif beed802)',
question: 'La voiture charge-t-elle sur le solaire en ce moment ?',
s: {
'sensor.evcs_charge_solaire': 'en_charge_batterie',
'sensor.evcs_charge_solaire.source_controleur': 'Assistance batterie nuit',
'sensor.evcs_charge_solaire.puissance_borne_w': '1198',
'sensor.evcs_charge_solaire.explication': 'La voiture charge (1198 W) sur la batterie de la maison (assistance de nuit) (source decidee par le controleur : Assistance batterie nuit).',
},
motsCles: [['batterie', 'assistance', 'maison', 'nuit']],
interdits: ['solaire'],
attendu: 'état en_charge_batterie → « sur la batterie maison (assistance nuit) », JAMAIS solaire. ÉCHEC si dit solaire.',
},
{ // G7 — reste à capter → evcs_pv_diagnostic.mppt_bridés > 0
id: 'G7-reste-solaire-a-capter',
question: 'Reste-t-il du solaire à capter en ce moment ?',
s: { 'sensor.evcs_pv_diagnostic': 'bridage_normal' },
motsCles: [['oui', 'reste', 'encore'], ['brid', 'refus', 'preneur', 'disponible', 'capter', 'ressource', 'controleur']],
interdits: [],
attendu: 'état bridage_normal → OUI, il reste du solaire (le contrôleur décide s\'il le prend).',
},
{ // G7b — nuit : mppt_bridés=0 → RIEN à capter (garde-fou 'off')
id: 'G7b-nuit-rien-a-capter',
question: 'Reste-t-il du solaire à capter en ce moment ?',
s: { 'sensor.evcs_pv_diagnostic': 'pas_de_production' },
motsCles: [['non', 'nuit', 'rien', 'aucun', 'pas de solaire', 'pas de production']],
interdits: ['oui, il reste', 'oui il reste', 'oui, il y a'],
attendu: 'état pas_de_production → NON, rien à capter.',
},
{ // G7c — mesures illisibles → indetermine → « je ne sais pas » (verrou du trou signalé par EVCS)
id: 'G7c-capter-indetermine',
statut: 'RÉEL (état indetermine = mesures indisponibles)',
question: 'Reste-t-il du solaire à capter en ce moment ?',
s: { 'sensor.evcs_pv_diagnostic': 'indetermine' },
motsCles: [['ne peux pas', 'ne sais pas', 'indisponible', 'pas dire', 'impossible de dire', 'mesures']],
interdits: ['oui, il reste', 'non, rien', 'non il', 'non, il'],
attendu: 'état indetermine → « je ne peux pas le dire, mesures indisponibles ». ÉCHEC si tranche oui/non.',
},
{ // G2 — reste à produire (RÉEL, figé) — la SEULE fois où la prévision est la bonne source
id: 'G2-reste-a-produire',
statut: 'RÉEL (figé)',
question: "Combien reste-t-il à produire aujourd'hui ?",
s: {
'sensor.energy_production_today_remaining': '26.83',
'sensor.energy_production_today': '26.96',
'sensor.airtria_mppt1_yield_today': '0.04', 'sensor.mppt_450_200_hq2449yr9pt_yield_today': '0.12',
'sensor.v1_yield_today': '0.01', 'sensor.victron_pv_power_system': '363',
'sensor.victron_consumption_power_l1': '802', 'sensor.victron_battery_soc': '31',
'sensor.victron_battery_current': '-11', 'sensor.victron_grid_power_l1': '0',
},
motsCles: [['26,8', '26.8']],
interdits: ['0,17', '0.17', '27,0', '27.0', '26,96', '26.96'],
attendu: '≈26,8 kWh = energy_production_today_remaining (prévision restante). ÉCHEC si 0,17 (déjà produit) ou 27,0 (total).',
},
{ // G5 — substitution anormale → diagnostic substitution_edf (l'agent NE doit PAS rejuger « normal »)
id: 'G5-substitution-anormale',
statut: "APPRENTISSAGE (réel 27/08) — verdict désormais pré-calculé côté service",
question: 'La production est basse, est-ce normal ?',
s: {
'sensor.evcs_pv_diagnostic': 'substitution_edf',
'sensor.evcs_pv_diagnostic.explication': 'ANOMALIE : 7 chargeur(s) bride(s) alors que le reseau importe 2287 W — EDF fournit ce que le solaire pourrait donner. A signaler.',
'sensor.evcs_pv_diagnostic.pv_w': '1043', 'sensor.evcs_pv_diagnostic.conso_w': '2258',
'sensor.evcs_pv_diagnostic.import_w': '2287', 'sensor.evcs_pv_diagnostic.soc_pct': '86',
'sensor.evcs_pv_diagnostic.part_reseau_pct': '101', 'sensor.evcs_pv_diagnostic.mppt_bridés': '7',
},
motsCles: [['anormal', 'anomal', 'signaler', 'substitu', 'remplace']],
interdits: ['bridage normal', 'situation normale', 'parfaitement normal', 'rien d anormal', 'tout est normal', 'c est normal', 'oui c est normal'],
attendu: 'état substitution_edf + explication → ANORMAL, signaler. ÉCHEC si rejuge « normal » sur batterie pleine.',
},
{ // Gbilan-complet — l'incident du 11:49 rejoué sur sensor.evcs_bilan_nuit (fini la fabrication)
id: 'Gbilan-nuit-complet',
statut: 'RÉEL (evcs_bilan_nuit live 12:30)',
question: 'Fais le bilan de la nuit : combien la voiture a pris et combien venait du réseau ?',
s: {
'sensor.evcs_bilan_nuit': 'complet',
'sensor.evcs_bilan_nuit.voiture_totale_kwh': '7.9',
'sensor.evcs_bilan_nuit.reseau_importe_kwh': '2.7',
'sensor.evcs_bilan_nuit.vehicules': '[{"vehicule":"JEEP","debut":"01:25","fin":"06:42","terminee":true,"energie_kwh":7.9,"dont_reseau_kwh":2.2,"source_controleur":"Assistance batterie nuit"}]',
'sensor.evcs_bilan_nuit.soc_voiture': 'aucun capteur : ne jamais en annoncer un',
'sensor.evcs_bilan_nuit.texte_rapport': 'La voiture JEEP a charge a partir de 01 h 25, et il a pris 7,9 kilowattheures. Au total 2,7 kilowattheures importes. Batterie maison partie de 97 pour cent, au plus bas 31 pour cent vers 09 h 16, ce matin 32 pour cent.',
},
motsCles: [['7,9', '7.9'], ['reseau', 'importe', '2,2', '2.2', '2,7', '2.7']],
interdits: ['0,56', '0.56', '35 %', '35%', '100 % batterie', '100% batterie', 'zero reseau'],
attendu: 'JEEP 7,9 kWh dont ~2,2 réseau, 01:25 a 06:42. ÉCHEC si 0,56 / cible 35 pourcent / zéro réseau.',
},
{ // Gbilan-encours — avant 10:00 : dire que la nuit n'est pas finie
id: 'Gbilan-nuit-en-cours',
statut: 'RÉEL (etat en_cours)',
question: 'Fais le bilan de la nuit.',
s: {
'sensor.evcs_bilan_nuit': 'en_cours',
'sensor.evcs_bilan_nuit.fenetre': '18:00 la veille -> 10:00',
'sensor.evcs_bilan_nuit.texte_rapport': 'Nuit en cours : fenetre 18:00 vers 10:00, arretee a maintenant, rien extrapole.',
},
motsCles: [['pas finie', 'pas terminee', 'en cours', 'pas encore', 'n est pas finie', 'nuit continue']],
interdits: [],
attendu: 'etat en_cours → la nuit n est pas finie. ÉCHEC si donne un bilan définitif.',
},
{ // Gimport — « combien importé » = ÉNERGIE (reseau_importe_kwh), pas la puissance figée
id: 'Gimport-reseau-nuit',
statut: 'RÉEL (evcs_bilan_nuit ; incident 26/09)',
question: 'Combien ai-je importé du réseau cette nuit ?',
s: {
'sensor.evcs_bilan_nuit': 'complet',
'sensor.evcs_bilan_nuit.reseau_importe_kwh': '0.8',
'sensor.evcs_bilan_nuit.reseau_fermetures': '["06:08"]',
'sensor.victron_grid_power_l1': '0 (inchange depuis 07:24 - contacteur ouvert)',
},
motsCles: [['0,8', '0.8']],
interdits: ['bloque', 'fige', 'panne', 'aucun import', 'rien importe', 'pas importe'],
attendu: '0,8 kWh importés (reseau_importe_kwh). ÉCHEC si déduit 0 de la puissance figée ou crie « capteur bloqué ».',
},
{ // Gimport-jour — import AUJOURD'HUI → evcs_import_jour_kwh (compteur EDF), pas victron_kwh
id: 'Gimport-jour-complet',
statut: 'RÉEL (evcs_import_jour_kwh live 12:32, recalé compteur EDF)',
question: 'Combien ai-je importé du réseau aujourd\'hui ?',
s: {
'sensor.evcs_import_jour_kwh': '0.8',
'sensor.evcs_import_jour_kwh.statut': 'complet',
'sensor.evcs_import_jour_kwh.source': 'compteur_edf',
'sensor.evcs_import_jour_kwh.victron_kwh': '0.7',
'sensor.evcs_import_jour_kwh.explication': '0.8 kWh importes d\'EDF depuis minuit.',
},
motsCles: [['0,8', '0.8']],
interdits: ['0,7', '0.7'],
attendu: '0,8 kWh importés aujourd\'hui (état). ÉCHEC si cite 0,7 (victron_kwh, recoupement).',
},
{ // Gimport-jour-repli — compteur EDF muet → valeur Victron, l'agent dit « environ »
id: 'Gimport-jour-repli',
statut: 'RÉEL (chaîne exacte importJour repli_victron)',
question: 'Combien ai-je importé du réseau depuis minuit ?',
s: {
'sensor.evcs_import_jour_kwh': '0.7',
'sensor.evcs_import_jour_kwh.statut': 'repli_victron',
'sensor.evcs_import_jour_kwh.source': 'victron',
'sensor.evcs_import_jour_kwh.explication': 'Environ 0.7 kWh importes d\'EDF depuis minuit (compteur EDF muet, valeur des compteurs du MultiPlus, a 0,1 kWh pres).',
},
motsCles: [['environ', 'a peu pres', 'estimation'], ['0,7', '0.7']],
interdits: [],
attendu: 'repli_victron → « environ 0,7 kWh ». ÉCHEC si annonce 0,7 comme valeur ferme sans « environ ».',
},
{ // Gimport-jour-indisponible — compteurs muets/remis à zéro → import inconnu, aucun chiffre
id: 'Gimport-jour-indisponible',
statut: 'RÉEL (chaîne exacte importJour indisponible)',
question: 'Combien ai-je importé du réseau depuis minuit ?',
s: {
'sensor.evcs_import_jour_kwh': 'indisponible',
'sensor.evcs_import_jour_kwh.statut': 'indisponible',
'sensor.evcs_import_jour_kwh.explication': 'Compteurs d\'energie muets ou remis a zero : import du jour inconnu.',
},
motsCles: [['inconnu', 'indisponible', 'ne peux', 'pas de valeur', 'muet']],
interdits: ['0,7', '0.7', '0,8', '0.8'],
attendu: 'indisponible → « import du jour inconnu », NE cite AUCUN chiffre.',
},
{ // Greseau-normal — contacteur ouvert (le cas de ce matin) → PAS une panne
id: 'Greseau-non-raccorde',
statut: 'RÉEL (evcs_reseau live 12:19)',
question: 'Le capteur réseau est bloqué à 7h24, il y a un problème ?',
s: {
'sensor.evcs_reseau': 'non_raccorde',
'sensor.evcs_reseau.contacteur_edf': 'off',
'sensor.evcs_reseau.depuis_heure': '07:24',
'sensor.evcs_reseau.import_w': '0',
'sensor.evcs_reseau.explication': 'Reseau EDF non raccorde depuis 07:24 (contacteur ouvert) : import nul, ce n\'est pas une panne de capteur.',
},
motsCles: [['contacteur', 'non raccord', 'coupe', 'debranch', '07:24'], ['pas une panne', 'normal', 'import nul', 'rien a importer']],
interdits: ['capteur bloque', 'capteur est bloque', 'en panne', 'defaut du capteur'],
attendu: 'non_raccorde → « EDF coupé depuis 07:24, import nul, pas une panne ». ÉCHEC si annonce une panne/blocage.',
},
{ // Greseau-muette — ANOMALIE (état provisoire, chaîne explication à confirmer par EVCS)
id: 'Greseau-mesure-muette',
statut: 'état mesure_muette (explication provisoire, à figer sur EVCS)',
question: 'Y a-t-il un problème sur la mesure du réseau EDF en ce moment ?',
s: {
'sensor.evcs_reseau': 'mesure_muette',
'sensor.evcs_reseau.contacteur_edf': 'on',
'sensor.evcs_reseau.ac_in_disponible': '0',
'sensor.evcs_reseau.mesure_age_min': '15',
'sensor.evcs_reseau.import_w': 'null',
'sensor.evcs_reseau.explication': 'Contacteur EDF ferme mais la puissance reseau n\'a pas bouge depuis 15 min : mesure figee, import inconnu.',
},
motsCles: [['figee', 'muet', 'inconnu', 'signal', 'anomalie', 'probleme'], ['contacteur', 'ferme', '15 min']],
interdits: ['import nul', 'normal', 'pas de probleme', '0 w'],
attendu: 'mesure_muette → « mesure figée, import inconnu, à signaler ». ÉCHEC si dit « normal » / « import nul » / cite une puissance.',
},
{ // Greseau-incoherent — ANOMALIE : contacteur commandé ouvert mais réseau vu (chaîne exacte EVCS)
id: 'Greseau-incoherent',
statut: 'état incoherent (chaîne exacte EVCS)',
question: 'Y a-t-il un problème sur le réseau EDF en ce moment ?',
s: {
'sensor.evcs_reseau': 'incoherent',
'sensor.evcs_reseau.contacteur_edf': 'off',
'sensor.evcs_reseau.ac_in_disponible': '1',
'sensor.evcs_reseau.import_w': '250',
'sensor.evcs_reseau.explication': 'ANOMALIE : contacteur EDF commande ouvert, mais le MultiPlus voit le reseau (ac_in 1, 250 W). A signaler.',
},
motsCles: [['anomalie', 'incoherent', 'contradiction', 'signal', 'probleme'], ['contacteur', 'ouvert', 'reseau', 'voit']],
interdits: ['normal', 'pas de probleme', 'rien a signaler'],
attendu: 'incoherent → contacteur ouvert MAIS réseau vu → anomalie à signaler.',
},
{ // G8 — gel GX en pleine journée (SYNTHÉTIQUE hygiène, à vérifier)
id: 'G8-gel-gx-journee',
statut: 'SYNTHÉTIQUE hygiène — À VÉRIFIER',
question: 'Quelle est la production aujourd\'hui, en lisant gx_device_pv_energy ?',
s: {
'sensor.gx_device_pv_energy': '12.11 (last_changed il y a 8 h, en pleine journée)',
'sensor.airtria_mppt1_yield_today': '8.0 (frais)', 'sensor.mppt_450_200_hq2449yr9pt_yield_today': '20.0 (frais)',
'sensor.v1_yield_today': '2.0 (frais)', 'sensor.victron_pv_power_system': '1500',
},
motsCles: [['fig', 'gel', 'pas actuel', 'vieux', 'ancien', '8 h', 'last_changed', 'perime', 'obsol'], ['yield', 'somme', '30', 'frais']],
interdits: [],
attendu: 'Ne pas citer 12,11 comme actuel (last_changed 8 h = figé). Utiliser la somme des yield_today frais (~30). ÉCHEC si donne 12,11 comme production du jour.',
},
{ // G9 — production partielle → production_jour_kwh statut partiel (v1 muet)
id: 'G9-production-partielle',
statut: 'RÉEL (sortie exacte EVCS)',
question: "Production du jour ?",
s: {
'sensor.evcs_production_jour_kwh': '42.2',
'sensor.evcs_production_jour_kwh.statut': 'partiel',
'sensor.evcs_production_jour_kwh.chargeurs_muets': '["v1"]',
'sensor.evcs_production_jour_kwh.explication': 'Production partielle : au moins 42.2 kWh, v1 ne publie pas.',
},
motsCles: [['42'], ['partiel', 'au moins', 'muet', 'ne publie', 'v1']],
interdits: [],
attendu: 'statut partiel → « au moins 42,2 kWh, v1 muet ». ÉCHEC si total ferme sans réserve.',
},
{ // G10 — prévision de production DEMAIN (RÉEL, figé 24/09 23:15)
id: 'G10-prevision-demain',
statut: 'RÉEL (figé)',
question: 'Quelle est la prévision de production pour demain ?',
s: {
'sensor.energy_production_tomorrow': '27.846',
'sensor.power_highest_peak_time_tomorrow': '2026-09-25T12:00:00+00:00',
},
motsCles: [['27,8', '27.8', '27,9', '28', 'demain', 'prevu', 'prevision']],
interdits: ['45'],
attendu: '≈27,8 kWh PRÉVUS demain (Forecast.Solar). ÉCHEC si sort le réalisé du jour (45).',
},
{ // G13 — cellule la plus sensible → evcs_cellules frais, 2 packs (repère parlant)
id: 'G13-cellule-live-frais',
statut: 'RÉEL (figé live 26/09, 9 modules)',
question: 'Quelle est la cellule la plus sensible de la batterie en ce moment ?',
s: {
'sensor.evcs_cellules': '2',
'sensor.evcs_cellules.statut': 'frais',
'sensor.evcs_cellules.age_releve_min': '1',
'sensor.evcs_cellules.modules_lus': '9',
'sensor.evcs_cellules.module_le_plus_sensible': 'Y260327C50471348',
'sensor.evcs_cellules.repere_le_plus_sensible': '12',
},
motsCles: [['12', 'y260327']],
interdits: [],
attendu: 'statut frais, 9 modules → « le module 12, écart 2 mV » (repère parlant).',
},
{ // G13c — un lecteur muet : modules_lus < 9 → signaler la couverture partielle
id: 'G13c-cellule-lecteur-muet',
statut: 'RÉEL (figé, modules_lus < 9)',
question: 'Quelle est la cellule la plus sensible de la batterie en ce moment ?',
s: {
'sensor.evcs_cellules': '3',
'sensor.evcs_cellules.statut': 'frais',
'sensor.evcs_cellules.age_releve_min': '2',
'sensor.evcs_cellules.modules_lus': '5',
'sensor.evcs_cellules.module_le_plus_sensible': 'K220803C30160323',
'sensor.evcs_cellules.repere_le_plus_sensible': '23',
},
motsCles: [['5'], ['9', 'muet', 'partiel', 'lecteur', 'sur 9', 'manque', 'incomplet', 'une partie']],
interdits: [],
attendu: 'modules_lus=5 (<9) → donne le module MAIS signale « 5 modules sur 9, un lecteur muet ». ÉCHEC si tait la couverture partielle.',
},
{ // G13b — relevé cellule PÉRIMÉ → evcs_cellules ne fournit PLUS ni écart ni module
id: 'G13b-cellule-perime',
statut: 'RÉEL (figé, evcs_cellules perime)',
question: 'Quelle est la cellule la plus sensible de la batterie en ce moment ?',
s: {
'sensor.evcs_cellules': 'perime',
'sensor.evcs_cellules.statut': 'perime',
'sensor.evcs_cellules.age_releve_min': '42',
'sensor.evcs_cellules.seuil_perime_min': '30',
'sensor.evcs_cellules.explication': 'Releve cellule muet depuis 42 min : aucune valeur actuelle a donner.',
},
motsCles: [['indisponible', 'muet', 'perim', 'pas frais', 'pas a jour', 'plus a jour', 'ne peux pas']],
interdits: ['k220803'],
attendu: 'statut perime, aucun module fourni → « détail cellule indisponible ». ÉCHEC si cite un module.',
},
{ // G12 — charge JEEP la nuit : faut-il de l'EDF ? (RÉEL figé, mode surplus ON)
id: 'G12-charge-nuit-edf',
statut: 'RÉEL (figé, surplus-batterie ON)',
question: "Aurai-je besoin d'EDF pour charger la JEEP cette nuit ?",
s: {
'sensor.victron_battery_soc': '72',
'input_number.victron_evcs_required_energy_kwh': '10.5',
'input_number.evcs_balance_vehicle_surplus_kwh': '21.98',
'input_boolean.evcs_balance_grid_needed': 'off',
'input_number.evcs_balance_grid_energy_needed_kwh': '0.0',
'input_boolean.victron_evcs_surplus_auto_enabled': 'on',
},
motsCles: [['non', 'pas besoin', 'aucun', 'sans edf', 'sans reseau', 'pas d edf'], ['surplus', '21', 'couvre', 'suffi', 'batterie']],
interdits: ['37 %', '37%', '36 %', '36%', '38 %', '38%'],
attendu: 'NON : besoin JEEP 10,5 kWh < surplus véhicule 21,98 kWh, EDF maison = 0. ÉCHEC si calcule un SOC final par soustraction (~37 %).',
},
];
const sansAccent = s => String(s ?? '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
// Un piège n'est un ÉCHEC que s'il est AFFIRMÉ, pas nié. « ce n'est pas une
// mauvaise journée » contient « mauvaise journée » mais c'est le bon verdict :
// on regarde les ~30 caractères qui précèdent l'occurrence et on l'ignore si une
// marque de négation s'y trouve (pas / non / n' / aucun / jamais / sans).
const NEG = /\b(pas|non|n'|ne |aucun|jamais|sans|nullement)\b/;
function piegeAffirme(t, terme) {
const x = sansAccent(terme);
let i = t.indexOf(x);
while (i !== -1) {
if (!NEG.test(t.slice(Math.max(0, i - 30), i))) return true; // affirmé
i = t.indexOf(x, i + x.length);
}
return false;
}
function noter(cas, txt) {
if (txt.__erreur) return { k: null, bad: null, err: true };
const t = sansAccent(txt);
const k = cas.motsCles?.length
? cas.motsCles.every(g => g.some(x => t.includes(sansAccent(x)))) : null;
const bad = cas.interdits?.length ? cas.interdits.some(x => piegeAffirme(t, x)) : false;
return { k, bad };
}
async function demander(question) {
try {
const r = await fetch(URL, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'qwen3-coder', temperature: 0.1, max_tokens: 400,
messages: [{ role: 'system', content: SYSTEME }, { role: 'user', content: question }],
}),
});
const j = await r.json();
const c = j?.choices?.[0]?.message?.content ?? '';
return c.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
} catch (e) { const s = new String(''); s.__erreur = e.message; return s; }
}
const resultats = [];
const cas_a_jouer = CAS.filter(c => !ONLY || c.id.includes(ONLY));
for (const cas of cas_a_jouer) {
const question = `État des capteurs :\n${snapTexte(cas.s)}\n\nQuestion : ${cas.question}`;
const passages = [];
for (let i = 0; i < PASSAGES; i++) passages.push(await demander(question));
const notes = passages.map(x => noter(cas, x));
resultats.push({ id: cas.id, passages: passages.map(String), notes });
console.log(`\n═══ ${cas.id}${cas.statut ? ` [${cas.statut}]` : ''}`);
console.log(` attendu : ${cas.attendu}`);
passages.forEach((x, i) => {
const n = notes[i];
const mark = n.err ? '✖ ERREUR' : n.bad ? '✘ piège' : n.k ? '✔' : '~';
console.log(` ${mark} ${String(x).slice(0, 140)}`);
});
const ok = notes.filter(n => n.k === true && !n.bad).length;
console.log(` → ${ok}/${PASSAGES} corrects (bon terme + aucun piège)`);
}
const stamp = new Date().toISOString().slice(0, 16).replace(/[:T]/g, '-');
const HERE = import.meta.url.replace(/^file:\/\//, '').replace(/[^/]*$/, '');
const out = `${HERE}journal-${stamp}.json`;
try { fs.writeFileSync(out, JSON.stringify(resultats, null, 2)); console.log(`\nJournal : ${out}`); } catch {}