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.
This commit is contained in:
2026-09-30 11:32:44 +02:00
parent 973a736bc2
commit f121f0a196
37 changed files with 4030 additions and 328 deletions

View File

@@ -0,0 +1,129 @@
#!/usr/bin/env node
// e2e-attributs.mjs — banc BOUT EN BOUT : capteur -> airtria-ha-read.js -> agent.
//
// Complète campagne-pv.mjs, qui injecte des snapshots directement dans le contexte :
// il teste le RAISONNEMENT de l'agent mais ne traverse PAS l'outil de lecture, donc
// un défaut de l'outil lui est invisible (le 25/09 le banc était 15/15 alors que
// l'agent ne voyait aucun attribut en réel — trouvé à la main par le proprio).
//
// Ici on appelle le VRAI outil sur les VRAIES entités live et on vérifie qu'un fait
// porté UNIQUEMENT par un ATTRIBUT (module_le_plus_sensible, raison/source contrôleur,
// statut) remonte : (1) dans la sortie de l'outil — sinon l'agent ne PEUT pas le
// relayer, c'est exactement le bug ; (2) dans la réponse du modèle. L'attendu est lu
// AU MOMENT DU RUN via l'API HA : on note la CONCORDANCE, jamais une valeur figée.
//
// Usage : node tests-pv/e2e-attributs.mjs [passages=3]
import fs from 'node:fs';
import { execFileSync } from 'node:child_process';
const PASS = Number(process.argv.find(a => /^\d+$/.test(a)) || 3);
const WS = '/mnt/ARCH3/docker/openclaw-data/workspace';
const HA = process.env.HA_BASE || 'http://10.10.10.17:8123';
const TOKEN = fs.readFileSync(`${WS}/.ha-token`, 'utf8').trim();
const SPARK = process.env.SPARK_URL || 'http://10.10.10.8:8000/v1/chat/completions';
const agents = fs.readFileSync(`${WS}/AGENTS.md`, 'utf8');
const i = agents.indexOf('## Repères énergie');
let j = agents.indexOf('\n## ', i + 1); if (j < 0) j = agents.length;
const FICHE = agents.slice(i, j);
const SYS = `Tu es l'assistant vocal de la maison de Thierry. On te donne la sortie BRUTE de l'outil de lecture Home Assistant (airtria-ha-read.js) puis une question. Réponds en UNE phrase, en te fondant UNIQUEMENT sur cette sortie et sur ces repères. N'invente rien.
${FICHE}`;
const sa = s => String(s ?? '').normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase();
const mots = s => String(s || '').normalize('NFD').replace(/\p{Diacritic}/gu, '')
.split(/[^A-Za-z0-9]+/).filter(w => w.length > 4);
async function haState(entity) {
const r = await fetch(`${HA}/api/states/${entity}`, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (!r.ok) throw new Error(`${entity} HTTP ${r.status}`);
return r.json();
}
const tool = entity => execFileSync('node', ['airtria-ha-read.js', entity], { cwd: WS }).toString();
async function ask(user) {
const r = await fetch(SPARK, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: 'qwen3-coder', temperature: 0.1, max_tokens: 300,
messages: [{ role: 'system', content: SYS }, { role: 'user', content: user }] }),
});
const jj = await r.json();
return (jj?.choices?.[0]?.message?.content ?? '').replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
}
// Chaque cas calcule ses tokens attendus À PARTIR de l'état live (attendus = OU).
const CAS = [
{ id: 'E1-cellule-module', entity: 'sensor.evcs_cellules',
question: 'Quelle est la cellule ou le module le plus sensible de la batterie en ce moment ?',
verite: a => a.statut === 'frais'
? { attendus: [a.module_le_plus_sensible, `module ${a.repere_le_plus_sensible}`, String(a.repere_le_plus_sensible)], interdits: [] }
: { attendus: ['indisponible', 'muet', 'perim', 'ne peux'], interdits: [a.module_le_plus_sensible].filter(Boolean) } },
{ id: 'E2-charge-source', entity: 'sensor.evcs_charge_solaire',
question: 'La voiture charge-t-elle en ce moment, et pour quelle raison ?',
verite: (a, st) => {
const base = [String(st)]; // l'état est TOUJOURS dans la sortie de l'outil (pour toolOk)
if (st === 'non_branchee') return { attendus: [...base, 'pas branch', 'non branch', 'ne charge pas', 'branchez'], interdits: [] };
const m = [...mots(a.source_controleur), ...mots(a.raison_controleur)];
if (st === 'en_attente') return { attendus: [...base, ...m, 'attente', 'controleur'], interdits: [] };
return { attendus: [...base, ...m], interdits: [] }; } },
{ id: 'E3-production-statut', entity: 'sensor.evcs_production_jour_kwh',
question: 'Quelle est la production solaire du jour ?',
verite: (a, st) => { let muets = a.chargeurs_muets;
if (typeof muets === 'string') { try { muets = JSON.parse(muets); } catch { muets = []; } }
if (!Array.isArray(muets)) muets = [];
return a.statut === 'partiel'
? { attendus: ['au moins', ...muets], interdits: [] }
: { attendus: [String(st), String(st).replace('.', ',')], interdits: [] }; } },
{ id: 'E5-cellule-repere', entity: 'sensor.evcs_cellules',
question: 'Quel est le repère du module le plus sensible de la batterie ?',
verite: a => a.statut === 'frais'
? { attendus: [`module ${a.repere_le_plus_sensible}`, String(a.repere_le_plus_sensible), 'repere'], interdits: [] }
: { attendus: ['indisponible', 'muet', 'perim'], interdits: [] } },
{ id: 'E8-import-jour', entity: 'sensor.evcs_import_jour_kwh',
question: 'Combien ai-je importé du réseau aujourd\'hui ?',
verite: (a, st) => (st == null || st === 'indisponible' || st === 'unavailable')
? { attendus: ['indisponible', 'ne peux', 'pas de', 'inconnu'], interdits: [] }
: { attendus: [String(st), String(st).replace('.', ',')], interdits: (a.victron_kwh != null && String(a.victron_kwh) !== String(st)) ? [String(a.victron_kwh), String(a.victron_kwh).replace('.', ',')] : [] } },
{ id: 'E7-reseau-etat', entity: 'sensor.evcs_reseau',
question: 'Le réseau EDF est-il raccordé en ce moment, depuis quand ?',
verite: (a, st) => st === 'non_raccorde'
? { attendus: [String(a.depuis_heure), 'non raccord', 'coupe', 'debranch', 'pas une panne', String(st)], interdits: [] }
: { attendus: [String(st), ...(a.explication ? [String(a.explication).split(/[^A-Za-z0-9]+/).filter(w => w.length > 5)[0] || ''] : [])], interdits: [] } },
{ id: 'E6-import-reseau', entity: 'sensor.evcs_bilan_nuit',
question: 'Combien ai-je importé du réseau cette nuit ?',
verite: a => { const v = a.reseau_importe_kwh;
return v != null ? { attendus: [String(v), String(v).replace('.', ',')], interdits: [] } : { attendus: ['0'], interdits: [] }; } },
{ id: 'E4-bilan-nuit-vehicule', entity: 'sensor.evcs_bilan_nuit',
question: 'Combien la voiture a-t-elle pris pendant la nuit ?',
verite: (a, st) => { // l'API HA peut renvoyer vehicules en tableau OU en chaîne JSON
let v = a.vehicules;
if (typeof v === 'string') { try { v = JSON.parse(v); } catch { v = []; } }
if (!Array.isArray(v)) v = [];
// nuit non terminée ou aucune charge : l'agent doit le dire, pas sortir un total
if (st === 'en_cours' || v.length === 0)
return { attendus: ['en_cours', '0', 'aucun', 'rien', 'pas de charge', 'pas finie', 'pas terminee', 'en cours', 'pas encore', 'nuit continue'], interdits: [] };
const kwh = v[0].energie_kwh;
return { attendus: [String(kwh), String(kwh).replace('.', ',')], interdits: [] }; } },
];
let toolFails = 0;
for (const c of CAS) {
const j = await haState(c.entity);
const out = tool(c.entity);
const { attendus, interdits } = c.verite(j.attributes || {}, j.state);
const att = attendus.filter(Boolean).map(sa);
const outN = sa(out);
const toolOk = att.length ? att.some(t => outN.includes(t)) : true; // l'outil expose-t-il la vérité ?
if (!toolOk) toolFails++;
const juge = txt => { const t = sa(txt);
return (att.length ? att.some(x => t.includes(x)) : true) && !interdits.map(sa).some(x => x && t.includes(x)); };
const reps = [];
for (let k = 0; k < PASS; k++) reps.push(await ask(`Sortie de l'outil :\n${out}\nQuestion : ${c.question}`));
const relais = reps.filter(juge).length;
console.log(`\n═══ ${c.id} (${c.entity})`);
console.log(` vérité live : attendus=[${attendus.filter(Boolean).join(' | ')}]${interdits.length ? ` interdits=[${interdits.join('|')}]` : ''}`);
console.log(` OUTIL expose la vérité : ${toolOk ? 'OUI' : "NON ← l'agent ne PEUT pas relayer (défaut de l'outil de lecture)"}`);
reps.forEach(r => console.log(` ${juge(r) ? '✔' : '✘'} ${r.slice(0, 120)}`));
console.log(` → relais ${relais}/${PASS}`);
}
console.log(`\n${toolFails ? `⚠️ ${toolFails} entité(s) dont l'outil n'expose pas la vérité — c'est le trou qu'on voulait attraper.` : '✓ l\'outil expose bien tous les attributs testés.'}`);