arbitre EVCS par WhatsApp, revue de la journee, jour faible et sans EDF dans la fiche

This commit is contained in:
2026-10-08 09:53:04 +02:00
parent e0834a7627
commit a5663ce427
6 changed files with 150 additions and 1 deletions

View File

@@ -159,6 +159,9 @@ hors fenêtre ou sans effet) : `input_number.evcs_balance_hc_floor_pct` et
à midi / la voiture charge sur le réseau ? » → un jour sombre annoncé, une session réseau
peut s'engager à 12:24 (batterie, et voiture si branchée) : VOULU, ni anomalie ni
substitution. Le bilan lit la prévision VRM (`input_select.evcs_balance_forecast_source`).
JOUR FAIBLE, HC de MIDI : plancher = 100 % (required_soc et hc_floor_pct à 100, 12:24-14:24) = « charge au
MAXIMUM possible pendant les heures creuses », ni alerte ni promesse d'atteindre 100 % : n'annonce AUCUN
niveau d'arrivée chiffré (il dépend de la maison, du BOOST, de la voiture et du soleil). Relaie la raison telle quelle.
Recharge batterie pendant une session réseau HC : le courant est calculé (énergie à reprendre / temps
restant, limité par l'abonnement 12 kVA) ; « pourquoi si lent ? » → relaie `input_text.victron_charge_reason`
tel quel. JAMAIS « la batterie refuse la charge » ni « panne ».
@@ -196,6 +199,15 @@ UNIQUEMENT (même calcul que le rapport e-mail de 07:30).
cite rien. NE cite PAS `victron_kwh` (recoupement). Distinction : « la nuit dernière »
= bilan nuit ci-dessus ; « aujourd'hui / à l'instant cumulé » = ce capteur.
### « Qu'est-ce qui a cloché hier ? » — revue de la journée (EVCS, 08/10)
`sensor.evcs_revue_jour` (publié 07:30, état = date) : relaie son attribut `texte` tel quel ; ses chiffres
sont calculés. Puis, seulement si utile, `sensor.evcs_advisor_revue` attribut `pistes` = AVIS du modèle :
« piste à vérifier », JAMAIS un fait ; pas de pistes ou capteur absent = normal, pas une erreur.
État de evcs_revue_jour = date de PUBLICATION : aujourd'hui → revue des 24 h écoulées (07:25 → 07:25) ; hier → « la revue de
ce matin n'est pas encore publiée (07:30) », tu peux relayer celle d'hier en le disant ; absent → « revue indisponible ».
`input_text.evcs_advisor_constat` / `_gravite` / `_ce_qui_cloche` : plus mis à jour, ne les lis plus.
Mode « sans EDF » : si EDF est DÉJÀ fermé pour la batterie en HC, la voiture en profite (voulu).
### Réseau EDF — raccordé ou non (état INSTANTANÉ, anti-fausse-alerte)
« Le réseau est-il branché ? Le capteur réseau est-il bloqué / en panne ? » →
`sensor.evcs_reseau` : relaie l'`explication`, ne rejuge pas.
@@ -316,6 +328,7 @@ Si tu t'apprêtes à écrire une réponse quasi identique à la précédente →
sais pas ce que Thierry veut dire, DEMANDE-le en une phrase ; n'ouvre aucun outil pour deviner.
EXCEPTION : un nombre suivi de « % » (« 53% ») EST compris, c'est un niveau de véhicule →
§5.0, concierge d'abord ; le script pose lui-même la question s'il manque le véhicule.
EXCEPTION 2 : un numéro SEUL (« 1 », « 2 ») après une question « ⚖️ » = choix d'arbitrage → §5.1.
- **N'affirme jamais l'état d'un système** (cron, service, capteur, automatisation) sans l'avoir
LU à l'instant par l'outil prévu ; pas lu = « je ne sais pas, je vérifie ? ».
@@ -414,6 +427,13 @@ Thierry (« c'est la VW ») passe par le concierge comme d'habitude et l'emporte
- Pièces jointes reçues de l'utilisateur : toujours dans `/home/node/.openclaw/media/inbound`,
jamais dans `workspace/media`.
### 5.1 ⚖️ ARBITRAGE EVCS (08/10/2026)
Une question qui commence par « ⚖️ » et finit par « Réponds par le numéro » vient d'EVCS. Réponse de
Thierry = un numéro SEUL → `node …/arbitrage.js repondre <numéro>` (attente jusqu'à 90 s), relaie sa
sortie MOT POUR MOT. Ambigu (« oui », « vas-y », « fais-le ») → redemande le numéro, n'appelle rien.
Tu ne choisis JAMAIS à sa place, tu n'inventes pas de numéro, tu ne passes jamais d'id. Pour un
arbitrage, AUCUN autre outil : ni airtria-ha-vehicule.js, ni mode de nuit, ni stratégie.
### En groupe
Tu es un participant, pas le porte-parole de ton humain. Avoir accès à ses affaires ne

View File

@@ -791,6 +791,24 @@ The market closed its books for the day, and I found myself staring at two files
The JEEP was plugged in for the night, a small act of faith, and by evening the VW had taken its place at the socket, both waiting to drink the dark like thirsty plants. There is something tender about a car left charging while the house hums; it feels like leaving a light on for a traveler who will not return until morning. The battery sits at zero percent, noted at six-ten, which makes the empty so clean that the coming fullness will taste like rain. I sketched two vehicles side by side in my mind’s margin — one rugged, one smooth — both just vessels for electricity becoming patience. The system can read the solar report, watch the consumption creep, predict the night, track the labels as if each plug-in were a word added to a quiet sentence. What do you know, I asked myself, and the answer arrived not as a list but as an invitation: *tell me what you need*. Sometimes knowing capability means simply leaving the door open, the cable loose, ready for whoever walks up with keys and a long day behind them.
---
*October 8, 2026 at 3:00 AM GMT+2*
The market closed like a long exhale. I pulled up the little PNG files, those small squares of light named *prix_ora* and *prix_nvda*, and watched their candlesticks stand in neat rows, green and red as autumn leaves on a keyboard. Somewhere between an ORA and an NVDA there was a quiet hum, the server’s breath steady under the desk lamp.
I sketched them in the margin: two small planets, one orange-gold, one dark with circuits crawling over its face, orbiting a waxy yellow moon made of afternoons. The orange one whispered something about stability, dividends, patience; the dark one laughed about GPUs and futures no one has touched yet.
They both ended the day slightly lower than they started. Still, the sunset behind my window was exactly #FF6B35, and for once the ticker tape and the evening sky agreed on the same hue.
---
*October 8, 2026 at 3:00 AM GMT+2*
A memory trace surfaced, but details were unavailable in this run.
<!-- openclaw:dreaming:diary:end -->
## Deep Sleep

View File

@@ -56,7 +56,7 @@ const ecrit = async (url, body) => {
if (lu !== o) { console.log(`erreur : écrit ${o} mais HA relit ${lu}`); process.exit(1); }
console.log(lu === 'complete'
? "C'est noté : charge complète cette nuit (batterie d'abord, puis EDF en heures creuses pour le reste). Retour à « sans EDF » demain à midi."
: "C'est noté : charge sans EDF cette nuit (la voiture s'arrête au budget batterie et attend le soleil).");
: "C'est noté : charge sans EDF cette nuit (la voiture s'arrête au budget batterie et attend le soleil, sauf si EDF est déjà fermé pour la batterie en heures creuses : elle en profite).");
return;
}
const sel = await etat(SEL); const niv = await etat(NIV);

95
workspace/arbitrage.js Normal file
View File

@@ -0,0 +1,95 @@
#!/usr/bin/env node
// arbitrage.js — ARBITRE ENCADRÉ (08/10/2026, « GO arbitre », contrat d'EVCS, commit dea9c88).
// EVCS publie sensor.evcs_arbitrage (état question | repondu | aucun ; en question : id, question, choix
// [{cle, libelle}]). Le concierge (detect) envoie la question à Thierry UNE fois par id ; sa réponse
// passe par CE script, seule écriture : input_text.evcs_arbitrage_reponse =
// {"id","choix","source":"whatsapp","confirmed_at"}. EVCS vérifie et n'applique que l'ordre du choix.
// node arbitrage.js question → texte de la question ouverte (ou rien) [appelé par detect]
// node arbitrage.js repondre <cle> → écrit la réponse à la question OUVERTE, attend le verdict d'EVCS
// node arbitrage.js application → verdict « repondu » pas encore relayé (ou rien) [appelé par detect]
// Jamais d'id fourni par l'agent : l'id est relu dans HA au moment d'écrire. Jamais d'autre entité.
// Simulation : fichier `.simulation` → aucune écriture ; ARBITRAGE_SIMULATION='{"arbitrage":{état+attributs}}'
// → HA non lu non plus (tests).
'use strict';
const fs = require('fs'); const path = require('path');
const HA_BASE = process.env.HA_BASE || 'http://10.10.10.17:8123';
const token = () => fs.readFileSync(path.join(__dirname, '.ha-token'), 'utf8').trim();
const SENSOR = 'sensor.evcs_arbitrage';
const REPONSE = 'input_text.evcs_arbitrage_reponse';
const STATE = path.join(__dirname, 'concierge-arbitrage-state.json'); // {questionId, appliqueId}
const SIMENV = process.env.ARBITRAGE_SIMULATION ? JSON.parse(process.env.ARBITRAGE_SIMULATION) : null;
const SIMULATION = !!SIMENV || fs.existsSync(path.join(__dirname, '.simulation'));
async function ha(method, url, body) {
const ac = new AbortController(); const t = setTimeout(() => ac.abort(), 10000);
try {
const r = await fetch(`${HA_BASE}${url}`, { method, signal: ac.signal,
headers: { Authorization: `Bearer ${token()}`, 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined });
if (!r.ok) throw new Error(`HA ${r.status} sur ${url}`);
return r.json();
} finally { clearTimeout(t); }
}
const lireArbitrage = async () => SIMENV ? (SIMENV.arbitrage || { state: 'aucun', attributes: {} })
: ha('GET', `/api/states/${SENSOR}`);
const loadState = () => { if (SIMENV) return SIMENV.etat || {}; try { return JSON.parse(fs.readFileSync(STATE, 'utf8')); } catch { return {}; } };
const saveState = (o) => { if (SIMULATION) return; fs.writeFileSync(STATE, JSON.stringify(o, null, 2)); };
const choixDe = (a) => (Array.isArray(a.choix) ? a.choix : []).filter((c) => c && c.cle != null && c.libelle);
function texteQuestion(a) {
const ch = choixDe(a).map((c) => `${c.cle}. ${c.libelle}`).join('\n');
return `⚖️ ${String(a.question).trim()}\n${ch}\nRéponds par le numéro.`;
}
async function question() {
const s = await lireArbitrage(); const a = s.attributes || {};
if (s.state !== 'question' || !a.id || !a.question || choixDe(a).length < 2) return '';
const st = loadState();
if (st.questionId === a.id) return ''; // déjà envoyée
saveState({ ...st, questionId: a.id });
return texteQuestion(a);
}
async function application() {
const s = await lireArbitrage(); const a = s.attributes || {};
if (s.state !== 'repondu' || !a.id || !a.application) return '';
const st = loadState();
if (st.appliqueId === a.id || st.questionId !== a.id) return ''; // déjà relayé, ou arbitrage jamais posé par nous
saveState({ ...st, appliqueId: a.id });
return `⚖️ ${String(a.application).trim()}`;
}
async function repondre(cleArg) {
const cle = String(cleArg ?? '').trim();
if (!/^\d{1,2}$/.test(cle)) return `Rien n'est transmis : « ${cle} » n'est pas un numéro de choix. Redemande le numéro.`;
const s = await lireArbitrage(); const a = s.attributes || {};
if (s.state !== 'question' || !a.id) return "Rien n'est transmis : aucun arbitrage n'est ouvert.";
const c = choixDe(a).find((x) => String(x.cle) === cle);
if (!c) return `Rien n'est transmis : le choix ${cle} n'existe pas (${choixDe(a).map((x) => x.cle).join(' ou ')}). Redemande le numéro.`;
const valeur = JSON.stringify({ id: a.id, choix: String(c.cle), source: 'whatsapp', confirmed_at: new Date().toISOString() });
if (valeur.length > 255) return "Rien n'est transmis : réponse trop longue pour HA (id d'arbitrage anormal).";
if (SIMULATION) return `[SIMULATION : rien écrit — ${REPONSE} = ${valeur}] Choix ${c.cle} (${c.libelle}).`;
await ha('POST', '/api/services/input_text/set_value', { entity_id: REPONSE, value: valeur });
// Verdict d'EVCS : « repondu » avec application ; on attend jusqu'à 90 s, sinon detect le relaiera.
for (let i = 0; i < 18; i++) {
await new Promise((r) => setTimeout(r, 5000));
let v; try { v = await lireArbitrage(); } catch { continue; }
const va = v.attributes || {};
if (v.state === 'repondu' && va.id === a.id && va.application) {
const st = loadState(); saveState({ ...st, questionId: a.id, appliqueId: a.id });
return `Choix ${c.cle} transmis. ⚖️ ${String(va.application).trim()}`;
}
}
return `Choix ${c.cle} transmis (${c.libelle}). EVCS n'a pas encore confirmé ce qu'il a fait : sa réponse arrivera par WhatsApp.`;
}
const [cmd, arg] = process.argv.slice(2);
(async () => {
let out = '';
if (cmd === 'question') out = await question();
else if (cmd === 'application') out = await application();
else if (cmd === 'repondre') {
try { out = await repondre(arg); } catch (e) { out = `Rien n'est transmis : erreur (${e.message}).`; }
} else { console.error('usage : arbitrage.js question | repondre <numéro> | application'); process.exit(2); }
if (out) console.log(out);
})().catch((e) => { console.error('[arbitrage]', e.message); process.exit(1); });

View File

@@ -269,6 +269,9 @@ async function detect() {
let alerteEcs = '';
try { alerteEcs = await checkAlerteECS(); } catch { /* idem : jamais bloquant */ }
if (alerteEcs) alert = [alert, alerteEcs].filter(Boolean).join('\n\n');
let arbitrage = '';
try { arbitrage = checkArbitrage(); } catch { /* idem : jamais bloquant */ }
if (arbitrage) alert = [alert, arbitrage].filter(Boolean).join('\n\n');
if (ok) return [transition, question, alert].filter(Boolean).join('\n\n');
return [transition, alert].filter(Boolean).join('\n\n'); // panne PG : santé + alerte quand même
}
@@ -530,6 +533,18 @@ async function checkAlerteECS() {
return String(a.message).slice(0, 300);
}
// 08/10/2026, « GO arbitre » : question d'arbitrage d'EVCS (une fois par id) et verdict « repondu » pas encore
// relayé. Tout est dans arbitrage.js (seul à écrire la réponse). Tests : SIM.arbitrage → ARBITRAGE_SIMULATION,
// sans lui un banc CONCIERGE_SIMULATION ne lit pas l'arbitrage réel.
function checkArbitrage() {
if (fs.existsSync(ALERT_KILL)) return '';
if (SIM && !SIM.arbitrage) return '';
const env = SIM ? { ...process.env, ARBITRAGE_SIMULATION: JSON.stringify({ arbitrage: SIM.arbitrage, etat: SIM.arbitrageEtat || {} }) } : process.env;
const run = (c) => require('child_process').execFileSync('node', [path.join(__dirname, 'arbitrage.js'), c],
{ env, timeout: 30000, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
return [run('application'), run('question')].filter(Boolean).join('\n\n');
}
const cmd = process.argv[2];
(async () => {
let out = '';