102 lines
5.5 KiB
JavaScript
102 lines
5.5 KiB
JavaScript
// =====================================================================
|
|
// PILES DES CAPTEURS (10/10/2026, demande du proprietaire)
|
|
// Le capteur temperature/humidite de la SDB est reste muet six jours (piles)
|
|
// sans que personne ne le sache. Ce module repere, parmi les etats HA, les
|
|
// capteurs d'APPAREILS a pile dont la pile est faible ou qui ne publient plus ;
|
|
// le service d'historisation publie sensor.evcs_piles et SPARK previent par
|
|
// WhatsApp. Fonction pure (tests : tests/piles/run.mjs).
|
|
// =====================================================================
|
|
|
|
export const SEUIL_PCT = 20; // pile faible a 20 % et moins
|
|
// HYSTERESIS (13/10) : une alerte ne retombe qu'a 40 % et plus (en pratique : un
|
|
// changement de piles). GRENIER 13 -> 19 -> 6 % et Chambre Gerard ont fait
|
|
// partir 5 messages le 13/10, l'alerte disparaissant puis revenant autour de 20 %.
|
|
export const SEUIL_RETOUR_PCT = 40;
|
|
export const MUET_H = 24; // indisponible depuis plus de 24 h
|
|
// Pas des piles de capteurs : parc Victron, voiture, telephones, reglages GX.
|
|
const EXCLUS = /^(sensor|number|binary_sensor)\.(victron_|gx_device_|airtria_8_|jeep_|iphone_|ipad_|samsung_)/;
|
|
|
|
/**
|
|
* @param {Array} etats liste des etats HA ({entity_id, state, attributes, last_changed})
|
|
* @param {number} maintenantMs
|
|
* @param {string[]} ignorees entites que le proprietaire a demande d'ignorer
|
|
*/
|
|
// ACQUITTEMENT (13/10, demande du proprietaire) : « j'ai compris, je change les
|
|
// piles dans N jours » -> plus de rappel pour cet appareil jusqu'a la date dite.
|
|
// input_text.evcs_piles_acquittees, ecrit par le concierge (SPARK), 255 car. max.
|
|
// 14/10 : les identifiants complets ne tenaient pas (9 alertes ~ 370 car.) ->
|
|
// CLE COURTE (6 caracteres, sha1 de l'entite, attribut `cle`) et format GROUPE
|
|
// PAR DATE : « AAAA-MM-JJ:cle|cle,AAAA-MM-JJ:cle » (~75 car. pour 9 alertes).
|
|
// Valable jusqu'a la fin de ce jour (heure de Paris). Une cle d'un appareil qui
|
|
// n'est plus en alerte (piles changees) est sans effet.
|
|
import { createHash } from 'node:crypto';
|
|
export const cleCourte = (entite) => createHash('sha1').update(String(entite)).digest('hex').slice(0, 6);
|
|
export function lireAcquittements(texte) {
|
|
const out = {}; // cle courte -> { jusquAu, finMs }
|
|
for (const groupe of String(texte || '').split(',')) {
|
|
const m = /^\s*(\d{4}-\d{2}-\d{2})\s*:\s*([0-9a-f|\s]+)$/.exec(groupe);
|
|
if (!m) continue;
|
|
const fin = Date.parse(`${m[1]}T23:59:59+02:00`);
|
|
if (!Number.isFinite(fin)) continue;
|
|
for (const k of m[2].split('|').map((x) => x.trim()).filter((x) => /^[0-9a-f]{6}$/.test(x))) {
|
|
if (!out[k] || out[k].finMs < fin) out[k] = { jusquAu: m[1], finMs: fin };
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* @param {object} opts precedentes : entites en alerte au passage precedent (hysteresis)
|
|
* acquittements : resultat de lireAcquittements
|
|
*/
|
|
export function pilesAAlerter(etats, maintenantMs, ignorees = [], opts = {}) {
|
|
const ign = new Set(ignorees.map((x) => x.trim()).filter(Boolean));
|
|
const prec = new Set(opts.precedentes || []);
|
|
const acq = opts.acquittements || {};
|
|
const alertes = [];
|
|
for (const s of etats) {
|
|
const id = s.entity_id;
|
|
if (!id.startsWith('sensor.') || EXCLUS.test(id) || ign.has(id)) continue;
|
|
const a = s.attributes || {};
|
|
const enPct = a.device_class === 'battery' && (a.unit_of_measurement === '%' || a.unit_of_measurement === undefined);
|
|
const enEnum = /_etat_batterie$/.test(id);
|
|
if (!enPct && !enEnum) continue;
|
|
const nom = String(a.friendly_name || id).replace(/\s+/g, ' ').trim();
|
|
const v = String(s.state ?? '').trim().toLowerCase();
|
|
const depuisMs = Date.parse(s.last_changed);
|
|
if (v === 'unavailable' || v === 'unknown' || v === '') {
|
|
const h = Number.isFinite(depuisMs) ? (maintenantMs - depuisMs) / 3600000 : Infinity;
|
|
if (h > MUET_H) alertes.push({ entite: id, nom, statut: 'muet', niveau: null, depuis: s.last_changed,
|
|
texte: `${nom} ne publie plus depuis ${Number.isFinite(h) ? Math.round(h / 24) + ' j' : 'longtemps'} (pile vide ou appareil hors ligne)` });
|
|
continue;
|
|
}
|
|
if (enPct) {
|
|
const n = Number(v);
|
|
if (Number.isFinite(n) && (n <= SEUIL_PCT || (prec.has(id) && n < SEUIL_RETOUR_PCT))) alertes.push({ entite: id, nom, statut: 'faible', niveau: n, depuis: s.last_changed,
|
|
texte: `${nom} : pile a ${n} %` });
|
|
} else if (v === 'low' || (v === 'middle' && prec.has(id))) {
|
|
// Meme hysteresis pour les etats : Chambre Gerard battait entre low et
|
|
// middle le 13/10 (deux messages). L'alerte ne retombe qu'a « high ».
|
|
alertes.push({ entite: id, nom, statut: 'faible', niveau: 'low', depuis: s.last_changed, texte: `${nom} : pile faible` });
|
|
}
|
|
}
|
|
alertes.sort((x, y) => (x.statut === y.statut ? x.nom.localeCompare(y.nom) : x.statut === 'faible' ? -1 : 1));
|
|
for (const a of alertes) {
|
|
a.cle = cleCourte(a.entite);
|
|
const k = acq[a.cle];
|
|
a.acquittee = !!(k && k.finMs >= maintenantMs);
|
|
if (a.acquittee) a.jusqu_au = k.jusquAu;
|
|
}
|
|
const aRappeler = alertes.filter((a) => !a.acquittee);
|
|
const acquittees = alertes.filter((a) => a.acquittee);
|
|
const jour = (iso) => iso.split('-').reverse().slice(0, 2).join('/');
|
|
return {
|
|
alertes,
|
|
a_rappeler: aRappeler,
|
|
acquittees,
|
|
texte: !alertes.length ? 'Toutes les piles des capteurs sont bonnes.'
|
|
: [...aRappeler.map((x) => `- ${x.texte}`),
|
|
...(acquittees.length ? [`Deja vues, pas de rappel avant la date dite : ${acquittees.map((x) => `${x.nom} (jusqu'au ${jour(x.jusqu_au)})`).join(', ')}.`] : [])].join('\n'),
|
|
};
|
|
}
|