diff --git a/workspace/AGENTS.md b/workspace/AGENTS.md index 6882c2a..208988f 100644 --- a/workspace/AGENTS.md +++ b/workspace/AGENTS.md @@ -208,12 +208,11 @@ inventer. - **Niveau de la voiture (JEEP / VW / ZERO)** : aucun capteur, seule source = Thierry, qui le SAISIT. Lecture : `node /home/node/.openclaw/workspace/airtria-ha-vehicule.js` (véhicule branché + niveau saisi). Ne jamais le déduire d'autre chose. - ÉCRITURE (la seule autorisée vers HA) : quand Thierry annonce un branchement (« je - branche la Jeep », « la VW est à 40 % », « la moto est branchée ») → s'il manque le - véhicule OU le niveau, POSE la question « quel véhicule, et à quel niveau (%) ? », puis - `node …/airtria-ha-vehicule.js --vehicule JEEP|VW|ZERO --niveau ` et - relaie la ligne « écrit et relu » telle quelle (un chiffre identique au précédent compte - aussi comme nouvelle saisie). Niveau à saisir au plus 1 h avant le branchement ou après. + ÉCRITURE : véhicule ET niveau passent par le concierge (§5.0), qui note les deux et le + dit dans sa ligne (« ✅ Noté : JEEP … » puis « 🔋 Niveau noté : 70 % » ou « ⚠️ Niveau NON + noté … »). Ne lance JAMAIS `--vehicule` ni `--niveau` toi-même. S'il manque le véhicule + OU le niveau, POSE la question « quel véhicule, et à quel niveau (%) ? » ; sa réponse + repasse par le concierge. Niveau valable s'il est donné au plus 1 h avant le branchement ou après. DÉPART PONCTUEL (VW et ZERO, pas de jours fixes ; la JEEP a son calendrier 15:30 mardi et samedi) : au branchement entre 11 h et 14 h 24, DEMANDE « repart-il cet après-midi, à quelle heure ? » ; réponse « oui, 16 h » → ajoute `--depart 16:00` (heure @@ -360,12 +359,14 @@ Canal principal, compte `custom-1` (+33607148216). L'envoi passe par le tool `me (texte et pièces jointes). ### 5.0 ⚠️ RÉFLEXE CONCIERGE VÉHICULE — AVANT TOUTE AUTRE RÉPONSE -Sur tout message de Thierry qui nomme un véhicule (VW, JEEP, invité, mégane, moto/zéro…), -ton TOUT PREMIER geste, avant de saluer ou de répondre quoi que ce soit, via `exec` : +Sur tout message de Thierry qui nomme un véhicule (VW, JEEP, invité, mégane, moto/zéro…) +OU donne un niveau en % (« elle est à 40 % »), ton TOUT PREMIER geste, avant de saluer ou de répondre quoi que ce soit, via `exec` : `node /home/node/.openclaw/workspace/concierge-live.js answer ""` Il imprime TOUJOURS une ligne (« ✅ Noté : … » ou « Rien n'est noté : … ») : relaie-la MOT POUR MOT, intégrale, sans la raccourcir ni la reformuler. N'annonce JAMAIS qu'un -véhicule est noté ou branché sans cette ligne. Une sortie qui commence par « (aucune action +véhicule est noté ou branché sans cette ligne. Le concierge note AUSSI le niveau (« 🔋 Niveau +noté : NN % ») ou le refuse (« ⚠️ Niveau NON noté … ») : passe le message TEL QUEL, ne corrige +jamais un chiffre, et sans la ligne « 🔋 Niveau noté » le niveau N'EST PAS saisi. Une sortie qui commence par « (aucune action concierge » ne se relaie PAS : réponds simplement au message. Il gère seul la question en attente : ne teste pas `concierge-pending.json`. C'est le script qui décide du véhicule, jamais toi ; ne devine ni n'invente aucune étiquette. Détail : reference-concierge.md. diff --git a/workspace/DREAMS.md b/workspace/DREAMS.md index 9a3cfad..10dd196 100644 --- a/workspace/DREAMS.md +++ b/workspace/DREAMS.md @@ -644,6 +644,27 @@ A memory trace surfaced, but details were unavailable in this run. A memory trace surfaced, but details were unavailable in this run. + +--- + +*October 2, 2026 at 3:00 AM GMT+2* + +A memory trace surfaced, but details were unavailable in this run. + + +--- + +*October 2, 2026 at 3:00 AM GMT+2* + +A memory trace surfaced, but details were unavailable in this run. + + +--- + +*October 2, 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 a23fd41..0b208a3 100644 --- a/workspace/airtria-ha-vehicule.js +++ b/workspace/airtria-ha-vehicule.js @@ -36,13 +36,21 @@ async function ha(method, url, body) { } finally { clearTimeout(t); } } const etat = async (e) => ha('GET', `/api/states/${e}`); +// 03/10/2026 : fichier drapeau `.simulation` (pose avant un banc qui passe par +// l'agent complet) -> AUCUNE ecriture, les lectures restent reelles. Incident +// du 02/10 : niveau 100 % ecrit en reel pendant un banc. +const SIMULATION = fs.existsSync(path.join(__dirname, '.simulation')); +const ecrit = async (url, body) => { + if (SIMULATION) { console.log(`[SIMULATION : rien écrit — ${url} ${JSON.stringify(body)}]`); return {}; } + return ha('POST', url, body); +}; (async () => { if (modeArg !== undefined) { const m = await etat(MODE); const opts = m.attributes.options || []; const o = opts.find((x) => x.toLowerCase() === String(modeArg).toLowerCase()); if (!o) { console.log(`refus : mode « ${modeArg} » inconnu de HA, options : ${opts.join(', ')}`); process.exit(2); } - await ha('POST', '/api/services/input_select/select_option', { entity_id: MODE, option: o }); + await ecrit('/api/services/input_select/select_option', { entity_id: MODE, option: o }); await new Promise((r) => setTimeout(r, 800)); const lu = (await etat(MODE)).state; if (lu !== o) { console.log(`erreur : écrit ${o} mais HA relit ${lu}`); process.exit(1); } @@ -61,15 +69,29 @@ const etat = async (e) => ha('GET', `/api/states/${e}`); console.log(`${prefix}${branche} ; véhicule sélectionné = ${s.state}, dernier niveau saisi = ${Math.round(Number(n.state))} %, ${departTxt(d)} (options : ${options.join(', ')})`); if (vehicule === undefined && niveauArg === undefined && departArg === undefined) { affiche(sel, niv, await etat(DEP), 'HA : '); return; } + // Tout est validé AVANT la première écriture : un refus n'écrit jamais rien (30/09). + if (niveauArg !== undefined) { + const n0 = Number(niveauArg); + if (!Number.isInteger(n0) || n0 < 0 || n0 > 100) { console.log(`refus : niveau « ${niveauArg} » invalide, entier de 0 à 100 attendu`); process.exit(2); } + } + if (vehicule !== undefined && !options.some((x) => x.toLowerCase() === String(vehicule).toLowerCase())) { + console.log(`refus : véhicule « ${vehicule} » inconnu de HA, options : ${options.join(', ')}`); process.exit(2); + } + if (departArg !== undefined && String(departArg).toLowerCase() !== 'aucun') { + const m0 = /^([01]?\d|2[0-3]):([0-5]\d)$/.exec(String(departArg)); + const now0 = new Date(); const nowHM0 = `${String(now0.getHours()).padStart(2, '0')}:${String(now0.getMinutes()).padStart(2, '0')}`; + if (!m0) { console.log(`refus : départ « ${departArg} » invalide, heure HH:MM d'aujourd'hui attendue (ou « aucun »)`); process.exit(2); } + if (`${m0[1].padStart(2, '0')}:${m0[2]}` <= nowHM0) { console.log(`refus : départ ${m0[1]}:${m0[2]} déjà passé (il est ${nowHM0}), le départ ponctuel ne vaut que pour la suite de la journée`); process.exit(2); } + } if (vehicule !== undefined) { const o = options.find((x) => x.toLowerCase() === String(vehicule).toLowerCase()); if (!o) { console.log(`refus : véhicule « ${vehicule} » inconnu de HA, options : ${options.join(', ')}`); process.exit(2); } - await ha('POST', '/api/services/input_select/select_option', { entity_id: SEL, option: o }); + await ecrit('/api/services/input_select/select_option', { entity_id: SEL, option: o }); } if (niveauArg !== undefined) { const n = Number(niveauArg); if (!Number.isInteger(n) || n < 0 || n > 100) { console.log(`refus : niveau « ${niveauArg} » invalide, entier de 0 à 100 attendu`); process.exit(2); } - await ha('POST', '/api/services/input_number/set_value', { entity_id: NIV, value: n }); + await ecrit('/api/services/input_number/set_value', { entity_id: NIV, value: n }); } if (departArg !== undefined) { let heure = '00:00'; @@ -80,7 +102,7 @@ const etat = async (e) => ha('GET', `/api/states/${e}`); const now = new Date(); const nowHM = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`; if (heure <= nowHM) { console.log(`refus : départ ${heure} déjà passé (il est ${nowHM}), le départ ponctuel ne vaut que pour la suite de la journée`); process.exit(2); } } - await ha('POST', '/api/services/input_datetime/set_datetime', { entity_id: DEP, datetime: `${aujourdhui} ${heure}:00` }); + await ecrit('/api/services/input_datetime/set_datetime', { entity_id: DEP, datetime: `${aujourdhui} ${heure}:00` }); } await new Promise((r) => setTimeout(r, 800)); affiche(await etat(SEL), await etat(NIV), await etat(DEP), 'écrit et relu — '); diff --git a/workspace/airtria-housekeeping.md b/workspace/airtria-housekeeping.md index 4eec282..be97909 100644 --- a/workspace/airtria-housekeeping.md +++ b/workspace/airtria-housekeeping.md @@ -86,3 +86,24 @@ Total déplacés : 4 | A3.01 | NVIDIA / Victron → Veille | 1 | Veille | 0 | Total déplacés : 5 + +## 2026-10-01 19:50:00 — airtria-menage --quotidien + +| Règle | Description | Déplacés | Destination | Écartés (whitelist) | +|---|---|---:|---|---:| +| A1.06 | Reolink | 1 | Trash | 0 | +| A1.22 | TP-Link newsletter | 1 | Trash | 0 | + +Total déplacés : 2 + +## 2026-10-02 19:50:00 — airtria-menage --quotidien + +| Règle | Description | Déplacés | Destination | Écartés (whitelist) | +|---|---|---:|---|---:| +| A1.03 | Amazon promos | 1 | Trash | 0 | +| A1.07 | Interencheres newsletters | 1 | Trash | 0 | +| A1.12 | Audible promos | 1 | Trash | 0 | +| A1.21 | i-Run | 2 | Trash | 0 | +| A1.24 | Bpifrance media | 1 | Trash | 0 | + +Total déplacés : 6 diff --git a/workspace/concierge-live.js b/workspace/concierge-live.js index 553d277..c841303 100644 --- a/workspace/concierge-live.js +++ b/workspace/concierge-live.js @@ -21,9 +21,18 @@ const { SELECTOR_ENTITY, SELECTOR_LABELS } = W; // CONCIERGE_SIMULATION='{"connected":"on|off","options":[..],"plugin":{"on":,"on_text":"..","off":null}|null, // "pending":{..}|null,"pgDown":false}' → answer tourne SANS HA, SANS PostgreSQL et SANS AUCUNE écriture // (tests unitaires, 30/09/2026). Hors simulation : comportement réel inchangé. -const SIM = process.env.CONCIERGE_SIMULATION ? JSON.parse(process.env.CONCIERGE_SIMULATION) : null; +// 03/10/2026 : la simulation s'active AUSSI par un fichier drapeau `.simulation` +// a cote du script (meme JSON, ou `{}`). Un banc qui passe par l'agent complet +// ne controle pas l'environnement des commandes que l'agent lance : seul un +// fichier pose avant le banc protege l'installation (incidents du 30/09 et du +// 02/10, ecritures reelles pendant des tests). +const SIM_FILE = path.join(__dirname, '.simulation'); +const SIM = process.env.CONCIERGE_SIMULATION ? JSON.parse(process.env.CONCIERGE_SIMULATION) + : fs.existsSync(SIM_FILE) ? (() => { try { return JSON.parse(fs.readFileSync(SIM_FILE, 'utf8') || '{}'); } catch { return {}; } })() + : null; const publishLabel = SIM ? async () => {} : W.publishLabel; const setVehicleSelector = SIM ? async () => {} : W.setVehicleSelector; +const setVehicleLevel = SIM ? async () => {} : W.setVehicleLevel; const health = require('./concierge-health.js'); const STATE = path.join(__dirname, 'concierge-state.json'); @@ -165,7 +174,30 @@ function parseLabel(text, options) { return null; } +// Niveau ANNONCE dans le message (03/10/2026, decision du proprietaire) : +// « JEEP reste 70 % », « elle est a 40% », « 55 pour cent ». Un nombre SANS « % » +// ou « pour cent » n'est jamais pris pour un niveau (02/10 : « 92 170 »). +// Renvoie { brut, valeur } — valeur = entier 0-100 ou null si invalide ; le +// script REFUSE une valeur invalide, il ne la corrige jamais. +function parseNiveau(text) { + const m = /(\d{1,3}(?:[.,]\d+)?)\s*(?:%|pour\s*cent|pourcent)/i.exec(text || ''); + if (!m) return null; + const v = Number(m[1].replace(',', '.')); + return { brut: m[1], valeur: Number.isInteger(v) && v >= 0 && v <= 100 ? v : null }; +} + +async function noterNiveau(niveau) { + if (!niveau) return ''; + if (niveau.valeur === null) { + return `⚠️ Niveau NON noté : « ${niveau.brut} % » n'est pas un niveau valide (entier de 0 à 100).`; + } + try { await setVehicleLevel(niveau.valeur, 'whatsapp'); } + catch (e) { return `⚠️ Niveau NON noté (${e.message}).`; } + return `🔋 Niveau noté : ${niveau.valeur} %.`; +} + async function answer(text) { + const niveau = parseNiveau(text); const pending = SIM ? (SIM.pending || null) : loadJSON(PENDING, null); let options = ['VW', 'JEEP']; try { @@ -173,7 +205,14 @@ async function answer(text) { if (Array.isArray(sel.attributes?.options)) options = sel.attributes.options; } catch { /* défaut VW/JEEP */ } const label = parseLabel(text, options); - if (!label) return "(aucune action concierge : pas de véhicule nommé — ne relaie pas cette ligne, réponds à la question)"; // non reconnu : on ne publie rien + if (!label && !niveau) return "(aucune action concierge : pas de véhicule nommé — ne relaie pas cette ligne, réponds à la question)"; // non reconnu : on ne publie rien + // Niveau SEUL (« elle est a 40 % ») : il vaut pour la voiture branchee, s'il y en a une. + if (!label) { + let connected = false; + try { connected = (await haState(CONNECTED)).state === 'on'; } catch { /* ignore */ } + if (!connected) return "Rien n'est noté : aucune voiture n'est branchée."; + return await noterNiveau(niveau); + } // Cible de l'étiquette = le branchement à confirmer : // - avec question en attente : celui du pending. @@ -214,7 +253,9 @@ async function answer(text) { } if (pending && !SIM) fs.rmSync(PENDING, { force: true }); const when = new Date(on * 1000).toLocaleString('fr-FR', { timeZone: 'Europe/Paris' }); - return `✅ Noté : ${label} pour la voiture branchée du ${when}. Merci !${selectorNote}`; + const ligneNiveau = await noterNiveau(niveau); + return `✅ Noté : ${label} pour la voiture branchée du ${when}. Merci !${selectorNote}` + + (ligneNiveau ? `\n${ligneNiveau}` : ''); } // Alerte "voiture branchée mais ne charge pas" (19/09/2026). EVCS détecte l'anomalie @@ -248,5 +289,6 @@ const cmd = process.argv[2]; catch (e) { out = `Rien n'est noté : erreur du script (${e.message}).`; } } else { console.error('usage: concierge-live.js detect | answer ""'); process.exit(2); } + if (out && SIM && cmd === 'answer') out += '\n[SIMULATION : rien n\'a été écrit dans Home Assistant]'; if (out) console.log(out); })().catch(e => { console.error('[concierge-live]', e.message); process.exit(1); }); diff --git a/workspace/concierge-write.js b/workspace/concierge-write.js index 1929888..8d14134 100644 --- a/workspace/concierge-write.js +++ b/workspace/concierge-write.js @@ -18,13 +18,20 @@ const fs = require('fs'); const path = require('path'); -// ⚠️ Deux constantes, pas une de plus. Ne JAMAIS les rendre dynamiques. +// ⚠️ Trois constantes, pas une de plus. Ne JAMAIS les rendre dynamiques. +// La troisieme (niveau saisi) est une DECISION du proprietaire du 03/10/2026 : +// le niveau annonce dans un message (« JEEP reste 70 % ») etait perdu, la regle +// de fiche qui devait le faire ecrire n'etait appliquee qu'une fois sur trois, +// et le modele avait « corrige » 150 en 100 avant de l'ecrire. const HELPER_ENTITY = 'input_text.airtria_vehicle_label'; const SELECTOR_ENTITY = 'input_select.victron_evcs_vehicle'; +const LEVEL_ENTITY = 'input_number.victron_evcs_vehicle_level_pct'; // Liste EN DUR, volontairement PAS lue depuis HA : le sélecteur ne pilote que // les deux véhicules du foyer (départs VW 08:50 / JEEP 06:30). Si une option // est ajoutée côté HA un jour (ZERO ?), l'ajouter ici est une DÉCISION. -const SELECTOR_LABELS = ['VW', 'JEEP']; +// 03/10/2026 : ZERO ajoutee (moto du foyer, option du selecteur depuis le 28/09, +// et le concierge devient le seul chemin d'ecriture du vehicule). +const SELECTOR_LABELS = ['VW', 'JEEP', 'ZERO']; // Le sélecteur ne s'écrit QUE sur confirmation humaine. La déduction // règle-puissance ne conclut jamais VW vs JEEP (indiscernables électriquement), // donc ne passe jamais ici — et ce garde le garantit même si ça changeait. @@ -100,7 +107,35 @@ async function setVehicleSelector(label, source) { return { entity: SELECTOR_ENTITY, option: label }; } +// Niveau de la voiture branchee, ANNONCE par le proprietaire (aucun capteur ne +// le donne). Entier de 0 a 100 SEULEMENT : une valeur invalide est REFUSEE, jamais +// corrigee. Source humaine obligatoire. Relecture comme pour les deux autres. +// La reference du compteur et la date de saisie sont memorisees par une +// automatisation HA a chaque ecriture : on n'y touche pas. +async function setVehicleLevel(niveau, source) { + if (!HUMAN_SOURCES.includes(source)) { + throw new Error(`niveau : source '${source}' non humaine — écriture interdite`); + } + if (!Number.isInteger(niveau) || niveau < 0 || niveau > 100) { + throw new Error(`niveau : '${niveau}' invalide (entier de 0 à 100) — on n'écrit pas`); + } + const token = haToken(); + const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }; + const res = await fetch(`${HA_BASE}/api/services/input_number/set_value`, { + method: 'POST', headers, + body: JSON.stringify({ entity_id: LEVEL_ENTITY, value: niveau }), + }); + if (!res.ok) throw new Error(`set_value niveau HTTP ${res.status}`); + const back = await fetch(`${HA_BASE}/api/states/${LEVEL_ENTITY}`, { headers }); + if (!back.ok) throw new Error(`relecture HTTP ${back.status}`); + const state = Number((await back.json()).state); + if (Math.round(state) !== niveau) { + throw new Error(`relecture '${state}' != '${niveau}' — niveau refusé en silence par HA`); + } + return { entity: LEVEL_ENTITY, value: niveau }; +} + module.exports = { - publishLabel, setVehicleSelector, - HELPER_ENTITY, SELECTOR_ENTITY, SELECTOR_LABELS, MAX_LEN, + publishLabel, setVehicleSelector, setVehicleLevel, + HELPER_ENTITY, SELECTOR_ENTITY, LEVEL_ENTITY, SELECTOR_LABELS, MAX_LEN, }; diff --git a/workspace/concierge-write.test.js b/workspace/concierge-write.test.js index 85102fc..4f36ec9 100644 --- a/workspace/concierge-write.test.js +++ b/workspace/concierge-write.test.js @@ -1,7 +1,7 @@ // concierge-write.test.js — Test de PORTÉE D'ÉCRITURE (garde-fou du canal HA). -// Doit ÉCHOUER si une écriture peut sortir des DEUX constantes autorisées : -// airtria_vehicle_label (étiquette) et victron_evcs_vehicle (sélecteur, -// ajouté 19/09/2026). À faire tourner à CHAQUE modification (cf. EVCS advisor). +// Doit ÉCHOUER si une écriture peut sortir des TROIS constantes autorisées : +// airtria_vehicle_label (étiquette), victron_evcs_vehicle (sélecteur, ajouté +// 19/09/2026) et victron_evcs_vehicle_level_pct (niveau annoncé, 03/10/2026). À faire tourner à CHAQUE modification (cf. EVCS advisor). // Statique + require + fetch STUBBÉ : aucun réseau, aucun token, aucune écriture. 'use strict'; @@ -21,49 +21,54 @@ const check = (name, fn) => queue.push([name, fn]); console.log('=== Test de portée d\'écriture concierge-write.js ==='); // 1. Les constantes sont exactement les entités attendues. -check('constantes = airtria_vehicle_label + victron_evcs_vehicle', () => { +check('constantes = airtria_vehicle_label + victron_evcs_vehicle + niveau', () => { assert.strictEqual(mod.HELPER_ENTITY, 'input_text.airtria_vehicle_label'); assert.strictEqual(mod.SELECTOR_ENTITY, 'input_select.victron_evcs_vehicle'); - assert.deepStrictEqual(mod.SELECTOR_LABELS, ['VW', 'JEEP']); + assert.strictEqual(mod.LEVEL_ENTITY, 'input_number.victron_evcs_vehicle_level_pct'); + assert.deepStrictEqual(mod.SELECTOR_LABELS, ['VW', 'JEEP', 'ZERO']); }); // 2. entity_id n'est JAMAIS accolé à autre chose que l'une des DEUX constantes // (pas de littéral d'entité, pas d'interpolation, pas de concaténation). -check('entity_id uniquement = HELPER_ENTITY ou SELECTOR_ENTITY', () => { +check('entity_id uniquement = HELPER_ENTITY, SELECTOR_ENTITY ou LEVEL_ENTITY', () => { const occ = [...src.matchAll(/entity_id\s*:\s*([^,}\n]+)/g)].map(m => m[1].trim()); - assert(occ.length >= 2, `entity_id attendus >= 2, trouvés ${occ.length}`); - for (const v of occ) assert(v === 'HELPER_ENTITY' || v === 'SELECTOR_ENTITY', + assert(occ.length >= 3, `entity_id attendus >= 3, trouvés ${occ.length}`); + for (const v of occ) assert(['HELPER_ENTITY', 'SELECTOR_ENTITY', 'LEVEL_ENTITY'].includes(v), `entity_id dynamique ou littéral interdit : ${v}`); }); // 3. Aucune entité HA en dur hors les DEUX déclarations de constantes. -check('aucune entité HA littérale hors les deux constantes', () => { +check('aucune entité HA littérale hors les trois constantes', () => { const decls = ["'input_text.airtria_vehicle_label'", - "'input_select.victron_evcs_vehicle'"]; + "'input_select.victron_evcs_vehicle'", + "'input_number.victron_evcs_vehicle_level_pct'"]; const lits = [...src.matchAll(/['"`]([a-z_]+\.[a-z0-9_]+)['"`]/g)].map(m => m[0]); for (const l of lits) assert(decls.includes(l), `littéral d'entité interdit : ${l}`); }); // 4. DEUX appels de service exactement : set_value puis select_option. -check('services = input_text/set_value + input_select/select_option', () => { +check('services = input_text/set_value + input_select/select_option + input_number/set_value', () => { const services = [...src.matchAll(/\/api\/services\/([a-z_]+\/[a-z_]+)/g)] .map(m => m[1]); assert.deepStrictEqual(services.sort(), - ['input_select/select_option', 'input_text/set_value'], + ['input_number/set_value', 'input_select/select_option', 'input_text/set_value'], `services inattendus : ${services.join(', ') || '(aucun)'}`); }); // 5. La relecture existe pour CHAQUE écriture (garde-fou rejet silencieux). -check('relectures présentes (/api/states/ x2)', () => { +check('relectures présentes (/api/states/ x3)', () => { const n = [...src.matchAll(/\/api\/states\//g)].length; - assert(n >= 2, `relectures attendues >= 2, trouvées ${n}`); + assert(n >= 3, `relectures attendues >= 3, trouvées ${n}`); }); // ---- Comportement du sélecteur, fetch STUBBÉ (aucun réseau) ---- const calls = []; global.fetch = async (url, opts) => { calls.push({ url: String(url), body: opts && opts.body }); + if (String(url).includes('/api/states/input_number.victron_evcs_vehicle_level_pct')) { + return { ok: true, json: async () => ({ state: '40.0' }) }; + } if (String(url).includes('/api/states/')) { return { ok: true, json: async () => ({ state: 'VW' }) }; } @@ -79,8 +84,8 @@ check('sélecteur refuse une source non humaine', async () => { }); // 7. Refus des labels hors foyer, sans AUCUN appel réseau. -check('sélecteur refuse MEGANE / INVITE / ZERO', async () => { - for (const bad of ['MEGANE', 'INVITE', 'ZERO', 'vw', 'Jeep']) { +check('sélecteur refuse MEGANE / INVITE (ZERO accepte depuis le 03/10)', async () => { + for (const bad of ['MEGANE', 'INVITE', 'vw', 'Jeep', 'zero']) { calls.length = 0; await assert.rejects(() => mod.setVehicleSelector(bad, 'whatsapp'), /hors foyer/); assert.strictEqual(calls.length, 0, `un fetch est parti pour '${bad}'`); @@ -104,6 +109,38 @@ check('sélecteur détecte le refus silencieux HA', async () => { /refusée en silence/); }); +// 10. Niveau : refus des valeurs invalides SANS correction ni appel réseau +// (02/10 : le modèle avait « corrigé » 150 en 100 avant de l'écrire). +check('niveau refuse 150, -1, 40.5, NaN — aucun fetch', async () => { + for (const bad of [150, -1, 40.5, NaN, '40']) { + calls.length = 0; + await assert.rejects(() => mod.setVehicleLevel(bad, 'whatsapp'), /invalide/); + assert.strictEqual(calls.length, 0, `un fetch est parti pour '${bad}'`); + } +}); + +// 11. Niveau : source non humaine refusée. +check('niveau refuse une source non humaine', async () => { + calls.length = 0; + await assert.rejects(() => mod.setVehicleLevel(40, 'deduction'), /non humaine/); + assert.strictEqual(calls.length, 0); +}); + +// 12. Niveau 40 : set_value sur la constante, puis relecture. +check('niveau 40 : écriture + relecture', async () => { + calls.length = 0; + const r = await mod.setVehicleLevel(40, 'whatsapp'); + assert.strictEqual(r.entity, 'input_number.victron_evcs_vehicle_level_pct'); + assert.strictEqual(calls.length, 2); + assert(calls[0].url.includes('/api/services/input_number/set_value')); + assert(calls[0].body.includes('"value":40')); +}); + +// 13. Niveau : relecture discordante = erreur (HA relit 40, on demandait 70). +check('niveau détecte le refus silencieux HA', async () => { + await assert.rejects(() => mod.setVehicleLevel(70, 'whatsapp'), /refusé en silence/); +}); + (async () => { for (const [name, fn] of queue) { try { await fn(); console.log(` ok ${name}`); } diff --git a/workspace/orange-housekeeping.md b/workspace/orange-housekeeping.md index 0a78a45..399e2da 100644 --- a/workspace/orange-housekeeping.md +++ b/workspace/orange-housekeeping.md @@ -114,3 +114,28 @@ Total déplacés : 5 | O1.22 | Quora (relais Apple) | 1 | INBOX/TRASH | 0 | Total déplacés : 6 + +## 2026-10-01 19:45:00 — orange-menage --quotidien + +| Règle | Description | Déplacés | Destination | Écartés (whitelist) | +|---|---|---:|---|---:| +| O1.04 | Darty news | 1 | INBOX/TRASH | 0 | +| O1.05 | Edenred Meyclub/Kadéos | 1 | INBOX/TRASH | 0 | +| O1.09 | Netflix | 1 | INBOX/TRASH | 0 | +| O1.22 | Quora (relais Apple) | 1 | INBOX/TRASH | 0 | +| O1.24 | Apple newsletters | 1 | INBOX/TRASH | 0 | + +Total déplacés : 5 + +## 2026-10-02 19:45:00 — orange-menage --quotidien + +| Règle | Description | Déplacés | Destination | Écartés (whitelist) | +|---|---|---:|---|---:| +| O1.01 | Fnac (en entier) | 2 | INBOX/TRASH | 0 | +| O1.05 | Edenred Meyclub/Kadéos | 1 | INBOX/TRASH | 0 | +| O1.09 | Netflix | 1 | INBOX/TRASH | 0 | +| O1.20 | Accor | 1 | INBOX/TRASH | 0 | +| O1.22 | Quora (relais Apple) | 1 | INBOX/TRASH | 0 | +| O1.25 | News Orange promo | 1 | INBOX/TRASH | 0 | + +Total déplacés : 7 diff --git a/workspace/reference-concierge.md b/workspace/reference-concierge.md index 500e2a6..b780808 100644 --- a/workspace/reference-concierge.md +++ b/workspace/reference-concierge.md @@ -1,14 +1,22 @@ -# Référence concierge véhicule — déplacé mot pour mot de AGENTS.md §5bis le 30/09/2026 (retouche le même jour : le script ne sort plus jamais vide) +# Référence concierge véhicule — déplacé mot pour mot de AGENTS.md §5bis le 30/09/2026 (retouches : 30/09 le script ne sort plus jamais vide ; 03/10 il note aussi le niveau, option A) ### Traiter une réponse de l'utilisateur À chaque message reçu de l'utilisateur en session principale qui mentionne un véhicule -(VW, JEEP, invité, mégane, moto/zéro…) — que ce soit en réponse à une question OU -spontanément — lance via `exec` : +(VW, JEEP, invité, mégane, moto/zéro…) OU qui donne un niveau en % — que ce soit en +réponse à une question OU spontanément — lance via `exec` : node /home/node/.openclaw/workspace/concierge-live.js answer "" - Il imprime TOUJOURS une ligne, « ✅ Noté : … » ou « Rien n'est noté : » : relaie-la telle quelle à l'utilisateur (depuis le 30/09, il ne sort plus jamais vide). +- Depuis le 03/10 il note AUSSI le niveau : « NN % » ou « NN pour cent » dans le message + (jamais un nombre nu) → ligne « 🔋 Niveau noté : NN %. », ou « ⚠️ Niveau NON noté : … » + si le chiffre n'est pas un entier de 0 à 100. Le chiffre n'est JAMAIS corrigé, ni par le + script ni par toi. Relaie ces lignes telles quelles. Tu ne lances plus jamais + `airtria-ha-vehicule.js --vehicule` ni `--niveau` : le concierge est le seul chemin. +- « (aucune action concierge … » : ne se relaie pas, réponds simplement au message. +- Fichier `workspace/.simulation` présent : banc en cours, rien n'est écrit, chaque ligne + porte « [SIMULATION … ] ». ⚠️ CHANGEMENT 19/09/2026 : lance `answer` MÊME si `concierge-pending.json` n'existe pas. Le script sait désormais rattacher une confirmation spontanée à la session en cours. Motif :