diff --git a/.gitignore b/.gitignore index 95571ea..514f0e5 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ workspace/* !workspace/bourse-config.json # code du concierge véhicule (aucun secret : lit les tokens depuis des fichiers ignorés) !workspace/concierge-*.js +!workspace/arbitrage.js # scripts Airtria (lecteurs lecture-seule PG/HA + harnais d'éval ; aucun secret embarqué) !workspace/airtria-*.js !workspace/airtria-*.mjs diff --git a/workspace/AGENTS.md b/workspace/AGENTS.md index 629afae..fb3a79d 100644 --- a/workspace/AGENTS.md +++ b/workspace/AGENTS.md @@ -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 ` (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 diff --git a/workspace/DREAMS.md b/workspace/DREAMS.md index ab8923c..22c03f3 100644 --- a/workspace/DREAMS.md +++ b/workspace/DREAMS.md @@ -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. + ## Deep Sleep diff --git a/workspace/airtria-ha-vehicule.js b/workspace/airtria-ha-vehicule.js index 0b208a3..4e2d1a1 100644 --- a/workspace/airtria-ha-vehicule.js +++ b/workspace/airtria-ha-vehicule.js @@ -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); diff --git a/workspace/arbitrage.js b/workspace/arbitrage.js new file mode 100644 index 0000000..807ad7d --- /dev/null +++ b/workspace/arbitrage.js @@ -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 → é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 | application'); process.exit(2); } + if (out) console.log(out); +})().catch((e) => { console.error('[arbitrage]', e.message); process.exit(1); }); diff --git a/workspace/concierge-live.js b/workspace/concierge-live.js index 8d5536f..eff445b 100644 --- a/workspace/concierge-live.js +++ b/workspace/concierge-live.js @@ -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 = '';