piles : rappel 09:00 et acquittement ; alertes certificats et EDF ; lecture autonomie VW ; dedoublonnage des releves
This commit is contained in:
@@ -272,6 +272,9 @@ async function detect() {
|
||||
let revue = '';
|
||||
try { revue = await checkAlerteRevue(); } catch { /* idem : jamais bloquant */ }
|
||||
if (revue) alert = [alert, revue].filter(Boolean).join('\n\n');
|
||||
let certs = '';
|
||||
try { certs = await checkCertificats(); } catch { /* idem : jamais bloquant */ }
|
||||
if (certs) alert = [alert, certs].filter(Boolean).join('\n\n');
|
||||
let piles = '';
|
||||
try { piles = await checkPiles(); } catch { /* idem : jamais bloquant */ }
|
||||
if (piles) alert = [alert, piles].filter(Boolean).join('\n\n');
|
||||
@@ -365,29 +368,42 @@ async function answerReleves(text, releves) {
|
||||
// 12/10/2026 : lecture-tableau.py passe CONCIERGE_RELEVE_SOURCE=photo_autonomie:<km>/<réf> quand le niveau est
|
||||
// déduit de l'autonomie (VW) ; il ne vaut que pour un relevé unique AVEC niveau.
|
||||
const srcAut = /^photo_autonomie:\d+\/\d+$/.test(process.env.CONCIERGE_RELEVE_SOURCE || '') ? process.env.CONCIERGE_RELEVE_SOURCE : null;
|
||||
// 12/10/2026 (EVCS) : un relevé photo est daté de la PHOTO (arrivée de l'image), pas de la confirmation :
|
||||
// la vue capacité prend le niveau de fin dans l'heure qui suit le débranchement.
|
||||
const tsEnv = Number(process.env.CONCIERGE_RELEVE_TS);
|
||||
const tsPhoto = releves.length === 1 && Number.isInteger(tsEnv) && tsEnv > maintenant - 2 * 86400 && tsEnv <= maintenant ? tsEnv : null;
|
||||
const aEcrire = releves.map((r, i) => ({ vehicule: r.vehicule, km: r.km, niveau: r.niveau,
|
||||
ts: i === 0 && rattache ? Math.round(ctx.on) : maintenant,
|
||||
ts: i === 0 && rattache ? Math.round(ctx.on) : (i === 0 && tsPhoto) || maintenant,
|
||||
...(srcAut && releves.length === 1 && r.niveau !== null ? { source: srcAut } : {}) }));
|
||||
// Dedoublonnage : meme vehicule et meme km deja dans la file (message relance,
|
||||
// ou compteur qui n'a pas bouge : un second releve identique n'apprend rien).
|
||||
let file = [];
|
||||
try { file = JSON.parse((await haState(W.RELEVE_ENTITY)).state || '[]'); } catch { /* vide */ }
|
||||
const nouveaux = aEcrire.filter(r => !file.some(e => Array.isArray(e) && e[0] === r.vehicule && e[1] === r.km));
|
||||
// 12/10/2026 (EVCS) : doublon = même véhicule + même km ET (même niveau, ou nouveau sans niveau). Un relevé AVEC
|
||||
// niveau là où l'existant n'en a pas est AJOUTÉ (son propre horodatage), sans remplacer l'ancien.
|
||||
const nouveaux = aEcrire.filter(r => !file.some(e => Array.isArray(e) && e[0] === r.vehicule && e[1] === r.km &&
|
||||
(r.niveau === null || e[2] === r.niveau)));
|
||||
if (nouveaux.length) {
|
||||
try { await setReleveKm(nouveaux, 'whatsapp'); }
|
||||
catch (e) { return (lignePrefixe ? lignePrefixe + ' — ' : '') + `⚠️ Relevé NON noté (${e.message}).`; }
|
||||
}
|
||||
const parties = aEcrire.map((r) => {
|
||||
if (!nouveaux.length) {
|
||||
const deja = aEcrire.map((r) => `${r.vehicule} ${kmTexte(r.km)} km${r.niveau === null ? '' : ` ${r.niveau} %`}`).join(' ; ');
|
||||
const ligneDeja = `🚗 Rien de nouveau à noter : ${deja} est déjà enregistré.`;
|
||||
return lignePrefixe ? `${lignePrefixe} — ${ligneDeja}` : ligneDeja;
|
||||
}
|
||||
const ecrits = new Set(nouveaux);
|
||||
const parties = aEcrire.filter((r) => ecrits.has(r)).map((r) => {
|
||||
let p = `${r.vehicule} ${kmTexte(r.km)} km`;
|
||||
p += r.niveau === null ? '' : `, niveau ${r.niveau} %`;
|
||||
if (rattache && r === aEcrire[0]) p += ` (rattaché au branchement de ${hhmm(ctx.on)})`;
|
||||
return p;
|
||||
});
|
||||
const sansNiveau = aEcrire.filter(r => r.niveau === null).map(r => r.vehicule);
|
||||
const sansNiveau = nouveaux.filter(r => r.niveau === null).map(r => r.vehicule);
|
||||
let ligne = `🚗 Relevé noté : ${parties.join(' ; ')}.`;
|
||||
if (sansNiveau.length) {
|
||||
ligne += ` ⚠️ Niveau non donné pour ${sansNiveau.join(', ')} : sans lui ce relevé ne servira pas au calcul ` +
|
||||
`(redonne-le avec le niveau, par exemple « ${sansNiveau[0]} ${kmTexte(aEcrire.find(r => r.niveau === null).km)} km 50 % »).`;
|
||||
`(redonne-le avec le niveau, par exemple « ${sansNiveau[0]} ${kmTexte(nouveaux.find(r => r.niveau === null).km)} km 50 % »).`;
|
||||
}
|
||||
return lignePrefixe ? `${lignePrefixe} — ${ligne}` : ligne;
|
||||
}
|
||||
@@ -565,7 +581,7 @@ async function checkAlerteRevue() {
|
||||
|
||||
// 10/10/2026, demande du propriétaire (via EVCS) : piles faibles et capteurs muets. EVCS calcule
|
||||
// sensor.evcs_piles (alertes [{entite, statut, texte}]) ; on relaie le `texte` de chaque alerte QUI APPARAÎT
|
||||
// (clé entite|statut), une fois, regroupées en un seul message. Une alerte disparue puis revenue repart.
|
||||
// (clé entite|statut), une fois, regroupées en un seul message, plus un rappel quotidien à 09:00 (14/10).
|
||||
// Envoi de 08:00 à 20:59 (heure de Paris) seulement : la nuit, les nouvelles alertes attendent le matin.
|
||||
// Capteur illisible ou absent → état local inchangé (pas de rafale au retour de HA).
|
||||
const PILES = 'sensor.evcs_piles';
|
||||
@@ -573,19 +589,61 @@ const PILES_STATE = path.join(__dirname, 'concierge-piles-state.json');
|
||||
async function checkPiles() {
|
||||
if (fs.existsSync(ALERT_KILL)) return '';
|
||||
let s;
|
||||
if (SIM) { if (!SIM.piles) return ''; s = { state: '1', attributes: { alertes: SIM.piles } }; }
|
||||
if (SIM) { if (!SIM.piles) return ''; s = { state: '1', attributes: Array.isArray(SIM.piles) ? { alertes: SIM.piles } : SIM.piles }; }
|
||||
else { try { s = await haState(PILES); } catch { return ''; } }
|
||||
const alertes = s?.attributes?.alertes;
|
||||
if (!Array.isArray(alertes) || s.state === 'unknown' || s.state === 'unavailable') return '';
|
||||
const heure = SIM?.heure ?? Number(new Intl.DateTimeFormat('fr-FR', { hour: 'numeric', hour12: false, timeZone: 'Europe/Paris' }).format(new Date()));
|
||||
const at = s?.attributes || {};
|
||||
if (!Array.isArray(at.alertes) || s.state === 'unknown' || s.state === 'unavailable') return '';
|
||||
const maintenant = SIM?.maintenant ? new Date(SIM.maintenant) : new Date();
|
||||
const heure = SIM?.heure ?? Number(new Intl.DateTimeFormat('fr-FR', { hour: 'numeric', hour12: false, timeZone: 'Europe/Paris' }).format(maintenant));
|
||||
if (heure < 8 || heure >= 21) return '';
|
||||
const jour = new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Paris' }).format(maintenant); // AAAA-MM-JJ
|
||||
const cle = (a) => `${a.entite}|${a.statut}`;
|
||||
const deja = new Set((SIM ? SIM.pilesState : loadJSON(PILES_STATE, {}))?.envoyees || []);
|
||||
const valides = alertes.filter((a) => a && a.entite && a.statut && a.texte);
|
||||
const nouvelles = valides.filter((a) => !deja.has(cle(a)));
|
||||
saveJSON(PILES_STATE, { envoyees: valides.map(cle) }); // les disparues sortent : elles repartiront si elles reviennent
|
||||
const st = (SIM ? SIM.pilesState : loadJSON(PILES_STATE, {})) || {};
|
||||
const deja = new Set(st.envoyees || []);
|
||||
const valides = at.alertes.filter((a) => a && a.entite && a.statut && a.texte);
|
||||
// 14/10/2026 (« GO piles ») : rappel QUOTIDIEN à 09:00 des alertes non acquittées (a_rappeler d'EVCS) ; dans la
|
||||
// journée, seules les alertes NOUVELLES non acquittées partent. « dernier » = clés du dernier message envoyé,
|
||||
// pour l'acquittement sans appareil nommé (piles-acquitter.js).
|
||||
const aRappeler = (Array.isArray(at.a_rappeler) ? at.a_rappeler : valides.filter((a) => !a.acquittee))
|
||||
.filter((a) => a && a.entite && a.texte);
|
||||
let titre = '', liste = [], rappel = st.rappel || null;
|
||||
if (heure >= 9 && st.rappel !== jour && aRappeler.length) {
|
||||
titre = '🔋 Rappel piles du jour :'; liste = aRappeler; rappel = jour;
|
||||
} else {
|
||||
liste = valides.filter((a) => !a.acquittee && !deja.has(cle(a))); titre = '🔋 Capteurs à vérifier :';
|
||||
}
|
||||
const dernier = liste.length ? liste.map((a) => a.cle || String(a.entite).replace(/^sensor\./, '')) : (st.dernier || []);
|
||||
saveJSON(PILES_STATE, { envoyees: valides.map(cle), rappel, dernier }); // les disparues sortent : elles repartiront si elles reviennent
|
||||
if (!liste.length) return '';
|
||||
return titre + '\n' + liste.map((a) => `- ${String(a.texte).trim()}`).join('\n') +
|
||||
'\nPour une pause : réponds « je change dans N jours » (ou « d\'ici samedi »).';
|
||||
}
|
||||
|
||||
// 13/10/2026, demande du propriétaire (via EVCS) : certificats HTTPS du reverse proxy. Même logique que les piles :
|
||||
// alertes NOUVELLES (clé domaine|statut) regroupées, de 08:00 à 20:59 ; une alerte urgente (expiré, ou ≤ 1 jour)
|
||||
// part aussi jusqu'à 22:59. On ajoute le geste à faire (dernière ligne du `texte` d'EVCS).
|
||||
const CERTS = 'sensor.evcs_certificats';
|
||||
const CERTS_STATE = path.join(__dirname, 'concierge-certificats-state.json');
|
||||
async function checkCertificats() {
|
||||
if (fs.existsSync(ALERT_KILL)) return '';
|
||||
let s;
|
||||
if (SIM) { if (!SIM.certs) return ''; s = { state: '1', attributes: SIM.certs }; }
|
||||
else { try { s = await haState(CERTS); } catch { return ''; } }
|
||||
const at = s?.attributes || {};
|
||||
if (!Array.isArray(at.alertes) || s.state === 'unknown' || s.state === 'unavailable') return '';
|
||||
const heure = SIM?.heure ?? Number(new Intl.DateTimeFormat('fr-FR', { hour: 'numeric', hour12: false, timeZone: 'Europe/Paris' }).format(new Date()));
|
||||
const urgente = (a) => a.statut === 'expire' || (a.statut === 'bientot' && Number(a.jours_restants) <= 1);
|
||||
const cle = (a) => `${a.domaine}|${a.statut}`;
|
||||
const valides = at.alertes.filter((a) => a && a.domaine && a.statut && a.texte);
|
||||
const deja = new Set((SIM ? SIM.certsState : loadJSON(CERTS_STATE, {}))?.envoyees || []);
|
||||
const ouvert = (a) => (heure >= 8 && heure < 21) || (urgente(a) && heure >= 8 && heure < 23);
|
||||
const nouvelles = valides.filter((a) => !deja.has(cle(a)) && ouvert(a));
|
||||
// Les envoyées restent tant qu'elles existent ; une hors plage horaire attend (non marquée).
|
||||
saveJSON(CERTS_STATE, { envoyees: valides.map(cle).filter((k) => deja.has(k) || nouvelles.some((a) => cle(a) === k)) });
|
||||
if (!nouvelles.length) return '';
|
||||
return '🔋 Capteurs à vérifier :\n' + nouvelles.map((a) => `- ${String(a.texte).trim()}`).join('\n');
|
||||
const geste = String(at.texte || '').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('-')).pop();
|
||||
return '🔒 Certificats HTTPS à renouveler :\n' + nouvelles.map((a) => `- ${String(a.texte).trim()}`).join('\n') +
|
||||
(geste ? `\n${geste}` : '');
|
||||
}
|
||||
|
||||
// 08/10/2026, « GO arbitre » : question d'arbitrage d'EVCS (une fois par id) et verdict « repondu » pas encore
|
||||
|
||||
Reference in New Issue
Block a user