workspace: niveau du vehicule par le concierge (option A), simulation par drapeau
- concierge-write.js : setVehicleLevel (entier 0-100, refus sans correction, relecture), ZERO dans SELECTOR_LABELS ; tests 13/13. - concierge-live.js answer : reconnait « NN % », ecrit le niveau, lignes « 🔋 Niveau noté » / « ⚠️ Niveau NON noté ». - Drapeau workspace/.simulation lu par concierge-live.js et airtria-ha-vehicule.js : aucune ecriture HA pendant un banc agent. - airtria-ha-vehicule.js : validation de tous les arguments avant ecriture. - AGENTS.md et reference-concierge.md : vehicule et niveau passent par le concierge. - Journaux de tri Orange et AIRTRIA, DREAMS.
This commit is contained in:
@@ -208,12 +208,11 @@ inventer.
|
|||||||
- **Niveau de la voiture (JEEP / VW / ZERO)** : aucun capteur, seule source = Thierry, qui
|
- **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
|
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.
|
branché + niveau saisi). Ne jamais le déduire d'autre chose.
|
||||||
ÉCRITURE (la seule autorisée vers HA) : quand Thierry annonce un branchement (« je
|
ÉCRITURE : véhicule ET niveau passent par le concierge (§5.0), qui note les deux et le
|
||||||
branche la Jeep », « la VW est à 40 % », « la moto est branchée ») → s'il manque le
|
dit dans sa ligne (« ✅ Noté : JEEP … » puis « 🔋 Niveau noté : 70 % » ou « ⚠️ Niveau NON
|
||||||
véhicule OU le niveau, POSE la question « quel véhicule, et à quel niveau (%) ? », puis
|
noté … »). Ne lance JAMAIS `--vehicule` ni `--niveau` toi-même. S'il manque le véhicule
|
||||||
`node …/airtria-ha-vehicule.js --vehicule JEEP|VW|ZERO --niveau <entier 0-100>` et
|
OU le niveau, POSE la question « quel véhicule, et à quel niveau (%) ? » ; sa réponse
|
||||||
relaie la ligne « écrit et relu » telle quelle (un chiffre identique au précédent compte
|
repasse par le concierge. Niveau valable s'il est donné au plus 1 h avant le branchement ou après.
|
||||||
aussi comme nouvelle saisie). Niveau à saisir 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
|
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
|
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
|
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).
|
(texte et pièces jointes).
|
||||||
|
|
||||||
### 5.0 ⚠️ RÉFLEXE CONCIERGE VÉHICULE — AVANT TOUTE AUTRE RÉPONSE
|
### 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…),
|
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` :
|
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 "<le texte exact du message reçu>"`
|
`node /home/node/.openclaw/workspace/concierge-live.js answer "<le texte exact du message reçu>"`
|
||||||
Il imprime TOUJOURS une ligne (« ✅ Noté : … » ou « Rien n'est noté : … ») : relaie-la
|
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
|
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
|
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 ;
|
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.
|
ne devine ni n'invente aucune étiquette. Détail : reference-concierge.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.
|
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.
|
||||||
|
|
||||||
<!-- openclaw:dreaming:diary:end -->
|
<!-- openclaw:dreaming:diary:end -->
|
||||||
|
|
||||||
## Deep Sleep
|
## Deep Sleep
|
||||||
|
|||||||
@@ -36,13 +36,21 @@ async function ha(method, url, body) {
|
|||||||
} finally { clearTimeout(t); }
|
} finally { clearTimeout(t); }
|
||||||
}
|
}
|
||||||
const etat = async (e) => ha('GET', `/api/states/${e}`);
|
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 () => {
|
(async () => {
|
||||||
if (modeArg !== undefined) {
|
if (modeArg !== undefined) {
|
||||||
const m = await etat(MODE); const opts = m.attributes.options || [];
|
const m = await etat(MODE); const opts = m.attributes.options || [];
|
||||||
const o = opts.find((x) => x.toLowerCase() === String(modeArg).toLowerCase());
|
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); }
|
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));
|
await new Promise((r) => setTimeout(r, 800));
|
||||||
const lu = (await etat(MODE)).state;
|
const lu = (await etat(MODE)).state;
|
||||||
if (lu !== o) { console.log(`erreur : écrit ${o} mais HA relit ${lu}`); process.exit(1); }
|
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(', ')})`);
|
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; }
|
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) {
|
if (vehicule !== undefined) {
|
||||||
const o = options.find((x) => x.toLowerCase() === String(vehicule).toLowerCase());
|
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); }
|
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) {
|
if (niveauArg !== undefined) {
|
||||||
const n = Number(niveauArg);
|
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); }
|
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) {
|
if (departArg !== undefined) {
|
||||||
let heure = '00:00';
|
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')}`;
|
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); }
|
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));
|
await new Promise((r) => setTimeout(r, 800));
|
||||||
affiche(await etat(SEL), await etat(NIV), await etat(DEP), 'écrit et relu — ');
|
affiche(await etat(SEL), await etat(NIV), await etat(DEP), 'écrit et relu — ');
|
||||||
|
|||||||
@@ -86,3 +86,24 @@ Total déplacés : 4
|
|||||||
| A3.01 | NVIDIA / Victron → Veille | 1 | Veille | 0 |
|
| A3.01 | NVIDIA / Victron → Veille | 1 | Veille | 0 |
|
||||||
|
|
||||||
Total déplacés : 5
|
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
|
||||||
|
|||||||
@@ -21,9 +21,18 @@ const { SELECTOR_ENTITY, SELECTOR_LABELS } = W;
|
|||||||
// CONCIERGE_SIMULATION='{"connected":"on|off","options":[..],"plugin":{"on":<epoch>,"on_text":"..","off":null}|null,
|
// CONCIERGE_SIMULATION='{"connected":"on|off","options":[..],"plugin":{"on":<epoch>,"on_text":"..","off":null}|null,
|
||||||
// "pending":{..}|null,"pgDown":false}' → answer tourne SANS HA, SANS PostgreSQL et SANS AUCUNE écriture
|
// "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é.
|
// (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 publishLabel = SIM ? async () => {} : W.publishLabel;
|
||||||
const setVehicleSelector = SIM ? async () => {} : W.setVehicleSelector;
|
const setVehicleSelector = SIM ? async () => {} : W.setVehicleSelector;
|
||||||
|
const setVehicleLevel = SIM ? async () => {} : W.setVehicleLevel;
|
||||||
const health = require('./concierge-health.js');
|
const health = require('./concierge-health.js');
|
||||||
|
|
||||||
const STATE = path.join(__dirname, 'concierge-state.json');
|
const STATE = path.join(__dirname, 'concierge-state.json');
|
||||||
@@ -165,7 +174,30 @@ function parseLabel(text, options) {
|
|||||||
return null;
|
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) {
|
async function answer(text) {
|
||||||
|
const niveau = parseNiveau(text);
|
||||||
const pending = SIM ? (SIM.pending || null) : loadJSON(PENDING, null);
|
const pending = SIM ? (SIM.pending || null) : loadJSON(PENDING, null);
|
||||||
let options = ['VW', 'JEEP'];
|
let options = ['VW', 'JEEP'];
|
||||||
try {
|
try {
|
||||||
@@ -173,7 +205,14 @@ async function answer(text) {
|
|||||||
if (Array.isArray(sel.attributes?.options)) options = sel.attributes.options;
|
if (Array.isArray(sel.attributes?.options)) options = sel.attributes.options;
|
||||||
} catch { /* défaut VW/JEEP */ }
|
} catch { /* défaut VW/JEEP */ }
|
||||||
const label = parseLabel(text, options);
|
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 :
|
// Cible de l'étiquette = le branchement à confirmer :
|
||||||
// - avec question en attente : celui du pending.
|
// - avec question en attente : celui du pending.
|
||||||
@@ -214,7 +253,9 @@ async function answer(text) {
|
|||||||
}
|
}
|
||||||
if (pending && !SIM) fs.rmSync(PENDING, { force: true });
|
if (pending && !SIM) fs.rmSync(PENDING, { force: true });
|
||||||
const when = new Date(on * 1000).toLocaleString('fr-FR', { timeZone: 'Europe/Paris' });
|
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
|
// 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}).`; }
|
catch (e) { out = `Rien n'est noté : erreur du script (${e.message}).`; }
|
||||||
}
|
}
|
||||||
else { console.error('usage: concierge-live.js detect | answer "<texte>"'); process.exit(2); }
|
else { console.error('usage: concierge-live.js detect | answer "<texte>"'); process.exit(2); }
|
||||||
|
if (out && SIM && cmd === 'answer') out += '\n[SIMULATION : rien n\'a été écrit dans Home Assistant]';
|
||||||
if (out) console.log(out);
|
if (out) console.log(out);
|
||||||
})().catch(e => { console.error('[concierge-live]', e.message); process.exit(1); });
|
})().catch(e => { console.error('[concierge-live]', e.message); process.exit(1); });
|
||||||
|
|||||||
@@ -18,13 +18,20 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
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 HELPER_ENTITY = 'input_text.airtria_vehicle_label';
|
||||||
const SELECTOR_ENTITY = 'input_select.victron_evcs_vehicle';
|
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
|
// 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
|
// 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.
|
// 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
|
// Le sélecteur ne s'écrit QUE sur confirmation humaine. La déduction
|
||||||
// règle-puissance ne conclut jamais VW vs JEEP (indiscernables électriquement),
|
// 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.
|
// 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 };
|
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 = {
|
module.exports = {
|
||||||
publishLabel, setVehicleSelector,
|
publishLabel, setVehicleSelector, setVehicleLevel,
|
||||||
HELPER_ENTITY, SELECTOR_ENTITY, SELECTOR_LABELS, MAX_LEN,
|
HELPER_ENTITY, SELECTOR_ENTITY, LEVEL_ENTITY, SELECTOR_LABELS, MAX_LEN,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// concierge-write.test.js — Test de PORTÉE D'ÉCRITURE (garde-fou du canal HA).
|
// 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 :
|
// Doit ÉCHOUER si une écriture peut sortir des TROIS constantes autorisées :
|
||||||
// airtria_vehicle_label (étiquette) et victron_evcs_vehicle (sélecteur,
|
// airtria_vehicle_label (étiquette), victron_evcs_vehicle (sélecteur, ajouté
|
||||||
// ajouté 19/09/2026). À faire tourner à CHAQUE modification (cf. EVCS advisor).
|
// 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.
|
// Statique + require + fetch STUBBÉ : aucun réseau, aucun token, aucune écriture.
|
||||||
'use strict';
|
'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 ===');
|
console.log('=== Test de portée d\'écriture concierge-write.js ===');
|
||||||
|
|
||||||
// 1. Les constantes sont exactement les entités attendues.
|
// 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.HELPER_ENTITY, 'input_text.airtria_vehicle_label');
|
||||||
assert.strictEqual(mod.SELECTOR_ENTITY, 'input_select.victron_evcs_vehicle');
|
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
|
// 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).
|
// (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());
|
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}`);
|
assert(occ.length >= 3, `entity_id attendus >= 3, trouvés ${occ.length}`);
|
||||||
for (const v of occ) assert(v === 'HELPER_ENTITY' || v === 'SELECTOR_ENTITY',
|
for (const v of occ) assert(['HELPER_ENTITY', 'SELECTOR_ENTITY', 'LEVEL_ENTITY'].includes(v),
|
||||||
`entity_id dynamique ou littéral interdit : ${v}`);
|
`entity_id dynamique ou littéral interdit : ${v}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 3. Aucune entité HA en dur hors les DEUX déclarations de constantes.
|
// 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'",
|
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]);
|
const lits = [...src.matchAll(/['"`]([a-z_]+\.[a-z0-9_]+)['"`]/g)].map(m => m[0]);
|
||||||
for (const l of lits) assert(decls.includes(l),
|
for (const l of lits) assert(decls.includes(l),
|
||||||
`littéral d'entité interdit : ${l}`);
|
`littéral d'entité interdit : ${l}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 4. DEUX appels de service exactement : set_value puis select_option.
|
// 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)]
|
const services = [...src.matchAll(/\/api\/services\/([a-z_]+\/[a-z_]+)/g)]
|
||||||
.map(m => m[1]);
|
.map(m => m[1]);
|
||||||
assert.deepStrictEqual(services.sort(),
|
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)'}`);
|
`services inattendus : ${services.join(', ') || '(aucun)'}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5. La relecture existe pour CHAQUE écriture (garde-fou rejet silencieux).
|
// 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;
|
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) ----
|
// ---- Comportement du sélecteur, fetch STUBBÉ (aucun réseau) ----
|
||||||
const calls = [];
|
const calls = [];
|
||||||
global.fetch = async (url, opts) => {
|
global.fetch = async (url, opts) => {
|
||||||
calls.push({ url: String(url), body: opts && opts.body });
|
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/')) {
|
if (String(url).includes('/api/states/')) {
|
||||||
return { ok: true, json: async () => ({ state: 'VW' }) };
|
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.
|
// 7. Refus des labels hors foyer, sans AUCUN appel réseau.
|
||||||
check('sélecteur refuse MEGANE / INVITE / ZERO', async () => {
|
check('sélecteur refuse MEGANE / INVITE (ZERO accepte depuis le 03/10)', async () => {
|
||||||
for (const bad of ['MEGANE', 'INVITE', 'ZERO', 'vw', 'Jeep']) {
|
for (const bad of ['MEGANE', 'INVITE', 'vw', 'Jeep', 'zero']) {
|
||||||
calls.length = 0;
|
calls.length = 0;
|
||||||
await assert.rejects(() => mod.setVehicleSelector(bad, 'whatsapp'), /hors foyer/);
|
await assert.rejects(() => mod.setVehicleSelector(bad, 'whatsapp'), /hors foyer/);
|
||||||
assert.strictEqual(calls.length, 0, `un fetch est parti pour '${bad}'`);
|
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/);
|
/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 () => {
|
(async () => {
|
||||||
for (const [name, fn] of queue) {
|
for (const [name, fn] of queue) {
|
||||||
try { await fn(); console.log(` ok ${name}`); }
|
try { await fn(); console.log(` ok ${name}`); }
|
||||||
|
|||||||
@@ -114,3 +114,28 @@ Total déplacés : 5
|
|||||||
| O1.22 | Quora (relais Apple) | 1 | INBOX/TRASH | 0 |
|
| O1.22 | Quora (relais Apple) | 1 | INBOX/TRASH | 0 |
|
||||||
|
|
||||||
Total déplacés : 6
|
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
|
||||||
|
|||||||
@@ -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
|
### Traiter une réponse de l'utilisateur
|
||||||
À chaque message reçu de l'utilisateur en session principale qui mentionne un véhicule
|
À 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
|
(VW, JEEP, invité, mégane, moto/zéro…) OU qui donne un niveau en % — que ce soit en
|
||||||
spontanément — lance via `exec` :
|
réponse à une question OU spontanément — lance via `exec` :
|
||||||
|
|
||||||
node /home/node/.openclaw/workspace/concierge-live.js answer "<texte exact du message>"
|
node /home/node/.openclaw/workspace/concierge-live.js answer "<texte exact du message>"
|
||||||
|
|
||||||
- Il imprime TOUJOURS une ligne, « ✅ Noté : … » ou « Rien n'est noté : <raison> » :
|
- Il imprime TOUJOURS une ligne, « ✅ Noté : … » ou « Rien n'est noté : <raison> » :
|
||||||
relaie-la telle quelle à l'utilisateur (depuis le 30/09, il ne sort plus jamais vide).
|
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.
|
⚠️ 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 :
|
Le script sait désormais rattacher une confirmation spontanée à la session en cours. Motif :
|
||||||
|
|||||||
Reference in New Issue
Block a user