Files
OPENCLAW_ARCHIVE3_17/workspace/piles-acquitter.js

110 lines
6.7 KiB
JavaScript

#!/usr/bin/env node
// piles-acquitter.js — ACQUITTEMENT d'une alerte de piles (14/10/2026, « GO piles », contrat d'EVCS commit ac1d006).
// Thierry répond à un message « 🔋 » par « je change dans 5 jours » / « d'ici samedi » : pas de rappel avant cette date.
// node piles-acquitter.js <délai> [appareil...]
// délai : N (jours), « demain », un jour de la semaine (« samedi » = le prochain), ou JJ/MM
// appareil : un mot du nom ou de la clé (« grenier », « sylvie ») ; AUCUN = toutes les alertes du DERNIER message
// Seule écriture : input_text.evcs_piles_acquittees = « AAAA-MM-JJ:cle|cle,… » (≤ 255 car.), groupes échus retirés
// d'abord, relecture après écriture. Le délai vient de Thierry, jamais du script : sans délai → refus.
// Simulation : fichier `.simulation` → aucune écriture ; PILES_SIMULATION='{"piles":{attributs},"helper":"…",
// "dernier":[…],"aujourdhui":"AAAA-MM-JJ"}' → ni lecture ni écriture HA (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_piles';
const HELPER = 'input_text.evcs_piles_acquittees';
const MAX = 255;
const STATE = path.join(__dirname, 'concierge-piles-state.json');
const SIMENV = process.env.PILES_SIMULATION ? JSON.parse(process.env.PILES_SIMULATION) : null;
const SIMULATION = !!SIMENV || fs.existsSync(path.join(__dirname, '.simulation'));
const JOURS = ['dimanche', 'lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi'];
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 aujourdhui = () => SIMENV?.aujourdhui || new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Paris' }).format(new Date());
const plusJours = (iso, n) => { const d = new Date(`${iso}T12:00:00Z`); d.setUTCDate(d.getUTCDate() + n); return d.toISOString().slice(0, 10); };
const jjmm = (iso) => `${iso.slice(8, 10)}/${iso.slice(5, 7)}`;
function echeance(arg) {
const a = String(arg || '').trim().toLowerCase();
const auj = aujourdhui();
if (/^\d{1,3}$/.test(a)) { const n = Number(a); return n >= 1 && n <= 180 ? plusJours(auj, n) : null; }
if (a === 'demain') return plusJours(auj, 1);
const j = JOURS.indexOf(a);
if (j >= 0) { const w = new Date(`${auj}T12:00:00Z`).getUTCDay(); return plusJours(auj, ((j - w + 7) % 7) || 7); }
const m = a.match(/^(\d{1,2})\/(\d{1,2})$/);
if (m) {
let an = Number(auj.slice(0, 4));
let iso = `${an}-${m[2].padStart(2, '0')}-${m[1].padStart(2, '0')}`;
if (Number.isNaN(Date.parse(iso))) return null;
if (iso <= auj) iso = `${an + 1}${iso.slice(4)}`;
return plusJours(auj, 180) >= iso ? iso : null;
}
return null;
}
(async () => {
const [delai, ...appareils] = process.argv.slice(2);
const fin = echeance(delai);
if (!fin) { console.log(`Rien n'est noté : délai « ${delai ?? ''} » non compris. Demande-le : « dans combien de jours changes-tu les piles ? »`); return; }
const piles = SIMENV ? { attributes: SIMENV.piles || {} } : await ha('GET', `/api/states/${SENSOR}`);
const alertes = (piles.attributes?.alertes || []).filter((a) => a && a.cle);
let cibles;
if (appareils.length) {
const mots = appareils.map((m) => m.toLowerCase());
cibles = alertes.filter((a) => mots.some((m) => a.cle.toLowerCase().includes(m) || String(a.nom || '').toLowerCase().includes(m)));
if (!cibles.length) { console.log(`Rien n'est noté : aucun appareil en alerte ne correspond à « ${appareils.join(' ')} ».`); return; }
} else {
const dernier = SIMENV ? (SIMENV.dernier || []) : (() => { try { return JSON.parse(fs.readFileSync(STATE, 'utf8')).dernier || []; } catch { return []; } })();
cibles = alertes.filter((a) => dernier.includes(a.cle));
if (!cibles.length) { console.log("Rien n'est noté : je ne retrouve pas le dernier message de piles. Précise l'appareil (ex. « grenier dans 5 jours »)."); return; }
}
const brut = SIMENV ? (SIMENV.helper ?? '') : (await ha('GET', `/api/states/${HELPER}`)).state;
const auj = aujourdhui();
// 14/10 (EVCS, commit 5d638f3) : format groupé « AAAA-MM-JJ:cle|cle,AAAA-MM-JJ:cle », cle = 6 hex (attribut cle).
// Lecture : date la plus lointaine par clé ; groupes échus (< aujourd'hui) retirés ; clés renouvelées remplacées.
const parDate = new Map(); // cle → date
for (const g of String(brut && brut !== 'unknown' && brut !== 'unavailable' ? brut : '').split(',')) {
const m = g.trim().match(/^(\d{4}-\d{2}-\d{2}):(.*)$/); // clé mal formée ignorée seule, pas le groupe
if (!m || m[1] < auj) continue;
for (const k of m[2].split('|')) if (/^[0-9a-f]{6}$/.test(k) && !(parDate.get(k) > m[1])) parDate.set(k, m[1]);
}
for (const a of cibles) parDate.delete(a.cle);
const serialise = (map) => {
const g = new Map();
for (const [k, d] of map) g.set(d, [...(g.get(d) || []), k]);
return [...g].sort(([a], [b]) => a.localeCompare(b)).map(([d, ks]) => `${d}:${ks.join('|')}`).join(',');
};
const notees = [], horsPlace = [];
for (const a of cibles) {
if (!/^[0-9a-f]{6}$/.test(a.cle)) { horsPlace.push(a); continue; }
parDate.set(a.cle, fin);
if (serialise(parDate).length <= MAX) notees.push(a); else { parDate.delete(a.cle); horsPlace.push(a); }
}
const liste = [serialise(parDate)].filter(Boolean);
const valeur = liste.join(',');
const noms = (l) => l.map((a) => a.nom || a.cle).join(', ');
let fait = '';
if (notees.length) {
if (SIMULATION) fait = ` [SIMULATION : rien écrit — ${HELPER} = ${valeur}]`;
else {
await ha('POST', '/api/services/input_text/set_value', { entity_id: HELPER, value: valeur });
await new Promise((r) => setTimeout(r, 800));
const lu = (await ha('GET', `/api/states/${HELPER}`)).state;
if (lu !== valeur) { console.log(`Rien n'est noté : écrit mais HA relit « ${String(lu).slice(0, 80)} ».`); return; }
}
}
let out = notees.length ? `🔋 Noté : pas de rappel pour ${noms(notees)} avant le ${jjmm(fin)}.` : "Rien n'est noté.";
if (horsPlace.length) out += ` ⚠️ Pas de place pour ${noms(horsPlace)} (limite HA de 255 caractères) : ils restent rappelés.`;
console.log(out + fait);
})().catch((e) => { console.log(`Rien n'est noté : erreur (${e.message}).`); process.exit(1); });