workspace: rapport solaire, prevision de nuit, concierge, courrier, fiche allegee
- Fiche agent maison (AGENTS.md) : prevision de nuit relayee par script, import
EDF, regle JEEP midi, saisie vehicule/niveau/depart, concierge qui relaie
« ✅ Noté » ou « Rien n'est noté », groupe A deplace (28 649 -> 25 831 car.).
- Nouvelles references : reference-concierge.md, reference-workspace.md.
- Scripts : rapport-solaire.js, prevision-nuit.js, airtria-ha-vehicule.js,
concierge-live.js (answer ne sort plus jamais vide, mode CONCIERGE_SIMULATION).
- Courrier Orange / AIRTRIA / Gmail : audits, regles, menage, recents.
- Bancs EVCS tests-pv (G5 accepte « anomal »).
- .gitignore : liste blanche etendue aux nouveaux scripts et bancs.
This commit is contained in:
@@ -16,13 +16,30 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { makeClient } = require('./concierge-detection.js');
|
||||
const { publishLabel } = require('./concierge-write.js');
|
||||
const W = require('./concierge-write.js');
|
||||
const { SELECTOR_ENTITY, SELECTOR_LABELS } = W;
|
||||
// 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
|
||||
// (tests unitaires, 30/09/2026). Hors simulation : comportement réel inchangé.
|
||||
const SIM = process.env.CONCIERGE_SIMULATION ? JSON.parse(process.env.CONCIERGE_SIMULATION) : null;
|
||||
const publishLabel = SIM ? async () => {} : W.publishLabel;
|
||||
const setVehicleSelector = SIM ? async () => {} : W.setVehicleSelector;
|
||||
const health = require('./concierge-health.js');
|
||||
|
||||
const STATE = path.join(__dirname, 'concierge-state.json');
|
||||
const PENDING = path.join(__dirname, 'concierge-pending.json');
|
||||
const HA_BASE = process.env.HA_BASE || 'http://10.10.10.17:8123';
|
||||
const SELECTOR = 'input_select.victron_evcs_vehicle';
|
||||
// Une seule source de vérité pour l'entité : celle du canal d'écriture.
|
||||
const SELECTOR = SELECTOR_ENTITY;
|
||||
const CONNECTED = 'binary_sensor.victron_evcs_vehicle_connected';
|
||||
// Alerte "branchée mais ne charge pas" : EVCS détecte et écrit ce helper, on relaie.
|
||||
const ALERT_HELPER = 'input_text.evcs_alerte_charge';
|
||||
// Interrupteur MAÎTRE, visible et contrôlé par le propriétaire depuis HA (créé par EVCS
|
||||
// le 19/09/2026). OFF par défaut (opt-in) tant que les seuils 500W/10min ne sont pas
|
||||
// calibrés. Rien ne part tant qu'il n'est pas 'on'.
|
||||
const ALERT_ENABLE = 'input_boolean.evcs_alerte_charge_enabled';
|
||||
const ALERT_STATE = path.join(__dirname, 'concierge-alert-state.json'); // dedup sur `since`
|
||||
const ALERT_KILL = path.join(__dirname, 'concierge-alert.disabled'); // 2e cran local d'urgence
|
||||
const WINDOW_DAYS = 5;
|
||||
const FRESH_BEFORE_S = 600, FRESH_AFTER_S = 1800;
|
||||
|
||||
@@ -48,6 +65,10 @@ const saveJSON = (f, o) => fs.writeFileSync(f, JSON.stringify(o, null, 2));
|
||||
const haToken = () => fs.readFileSync(process.env.HA_TOKEN_FILE || path.join(__dirname, '.ha-token'), 'utf8').trim();
|
||||
|
||||
async function haState(entity) {
|
||||
if (SIM) {
|
||||
if (entity === CONNECTED) return { state: SIM.connected };
|
||||
return { state: '', attributes: { options: SIM.options || ['VW', 'JEEP', 'ZERO'] } };
|
||||
}
|
||||
const r = await fetch(`${HA_BASE}/api/states/${entity}`, { headers: { Authorization: `Bearer ${haToken()}` } });
|
||||
if (!r.ok) throw new Error(`HA ${entity} HTTP ${r.status}`);
|
||||
return r.json();
|
||||
@@ -78,6 +99,10 @@ function selectorFresh(onEpoch, selChanges) {
|
||||
}
|
||||
|
||||
async function loadWindow() {
|
||||
if (SIM) {
|
||||
if (SIM.pgDown) throw new Error('PostgreSQL simulé indisponible');
|
||||
return { plugins: SIM.plugin ? [SIM.plugin] : [], selChanges: [] };
|
||||
}
|
||||
const until = new Date().toISOString();
|
||||
const since = new Date(Date.now() - WINDOW_DAYS * 864e5).toISOString();
|
||||
const c = makeClient();
|
||||
@@ -123,8 +148,10 @@ async function detect() {
|
||||
try { question = await runDetection(); }
|
||||
catch (e) { ok = false; detail = e.message; }
|
||||
const transition = await health.publish({ ok, detail });
|
||||
if (ok) return [transition, question].filter(Boolean).join('\n\n');
|
||||
return transition; // échec : WhatsApp seulement à la bascule vers panne
|
||||
let alert = '';
|
||||
try { alert = await checkAlert(); } catch { /* l'alerte ne casse jamais le cycle */ }
|
||||
if (ok) return [transition, question, alert].filter(Boolean).join('\n\n');
|
||||
return [transition, alert].filter(Boolean).join('\n\n'); // panne PG : santé + alerte quand même
|
||||
}
|
||||
|
||||
// Labels en MAJUSCULES sans accent (convention colonne : VW/JEEP/MEGANE). Le propriétaire
|
||||
@@ -139,26 +166,87 @@ function parseLabel(text, options) {
|
||||
}
|
||||
|
||||
async function answer(text) {
|
||||
const pending = loadJSON(PENDING, null);
|
||||
if (!pending) return '';
|
||||
const pending = SIM ? (SIM.pending || null) : loadJSON(PENDING, null);
|
||||
let options = ['VW', 'JEEP'];
|
||||
try {
|
||||
const sel = await haState(SELECTOR);
|
||||
if (Array.isArray(sel.attributes?.options)) options = sel.attributes.options;
|
||||
} catch { /* défaut VW/JEEP */ }
|
||||
const label = parseLabel(text, options);
|
||||
if (!label) return ''; // non reconnu : on ne publie rien
|
||||
await publishLabel(pending.branchement, label, 'whatsapp');
|
||||
fs.rmSync(PENDING, { force: true });
|
||||
const when = new Date(pending.on * 1000).toLocaleString('fr-FR', { timeZone: 'Europe/Paris' });
|
||||
return `✅ Noté : ${label} pour la voiture branchée du ${when}. Merci !`;
|
||||
if (!label) return "Rien n'est noté : ce message ne nomme pas de véhicule."; // non reconnu : on ne publie rien
|
||||
|
||||
// Cible de l'étiquette = le branchement à confirmer :
|
||||
// - avec question en attente : celui du pending.
|
||||
// - SANS question (message spontané, 19/09/2026) : la session EN COURS, si une
|
||||
// voiture est physiquement branchée. Le 19/09 le propriétaire a écrit « JEEP »
|
||||
// à 15h03 sans qu'aucune question soit posée (swap 14s non détecté) et l'info
|
||||
// était jetée par l'ancienne règle « pas de pending → rien ». Son message
|
||||
// compte désormais seul, tant qu'une voiture est là pour le rattacher.
|
||||
let branchement, on;
|
||||
if (pending) {
|
||||
branchement = pending.branchement;
|
||||
on = pending.on;
|
||||
} else {
|
||||
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."; // rien branché → aucune session à étiqueter
|
||||
let latest = null;
|
||||
try { const w = await loadWindow(); latest = w.plugins[w.plugins.length - 1] || null; }
|
||||
catch { return "Rien n'est noté : historique des branchements illisible (base indisponible), je ne devine pas."; } // PostgreSQL indisponible
|
||||
if (!latest || latest.off !== null) return "Rien n'est noté : aucun branchement en cours dans l'historique de la borne."; // pas de branchement en cours
|
||||
branchement = latest.on_text;
|
||||
on = latest.on;
|
||||
}
|
||||
await publishLabel(branchement, label, 'whatsapp');
|
||||
// Confirmation HUMAINE d'un véhicule du FOYER : le sélecteur temps réel suit
|
||||
// (il pilote l'heure de départ VW 08:50 / JEEP 06:30, donc la priorité de
|
||||
// nuit). Pour MEGANE/INVITE/ZERO on n'y touche pas. Demandé le 19/09/2026 :
|
||||
// « répondre WhatsApp PUIS changer le sélecteur à la main », c'est fini.
|
||||
// Si l'écriture échoue, l'étiquette est déjà publiée : on le DIT au lieu
|
||||
// d'échouer, pour que le propriétaire fasse le geste manuel en connaissance.
|
||||
let selectorNote = '';
|
||||
if (SELECTOR_LABELS.includes(label)) {
|
||||
try { await setVehicleSelector(label, 'whatsapp'); }
|
||||
catch (e) {
|
||||
selectorNote = `\n⚠️ Sélecteur véhicule NON mis à jour (${e.message}) — ` +
|
||||
`pense à le passer sur ${label} à la main.`;
|
||||
}
|
||||
}
|
||||
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}`;
|
||||
}
|
||||
|
||||
// Alerte "voiture branchée mais ne charge pas" (19/09/2026). EVCS détecte l'anomalie
|
||||
// (véhicule branché + borne quasi 0 W + état bloquant + excédent solaire, tenu > 10 min)
|
||||
// et écrit input_text.evcs_alerte_charge. Ici on relaie SON champ `message` sur WhatsApp
|
||||
// UNE fois par épisode — clef de dedup = `since` — derrière un kill-switch. Le helper
|
||||
// vaut '' / 'unknown' hors anomalie : rien ne part. Ne casse jamais le cycle detect.
|
||||
async function checkAlert() {
|
||||
if (fs.existsSync(ALERT_KILL)) return ''; // 2e cran local : coupure d'urgence
|
||||
// Interrupteur maître du propriétaire (HA). Illisible → on n'envoie pas (fail-safe :
|
||||
// en cas de doute, ne pas spammer).
|
||||
try { if ((await haState(ALERT_ENABLE)).state !== 'on') return ''; }
|
||||
catch { return ''; }
|
||||
let raw;
|
||||
try { raw = (await haState(ALERT_HELPER)).state; } catch { return ''; }
|
||||
if (!raw || raw === 'unknown' || raw === 'unavailable') return '';
|
||||
let a; try { a = JSON.parse(raw); } catch { return ''; }
|
||||
if (!a || a.active !== true || !a.since || !a.message) return '';
|
||||
const st = loadJSON(ALERT_STATE, {});
|
||||
if (st.lastSince === a.since) return ''; // déjà relayé pour cet épisode
|
||||
saveJSON(ALERT_STATE, { lastSince: a.since });
|
||||
return String(a.message).slice(0, 300);
|
||||
}
|
||||
|
||||
const cmd = process.argv[2];
|
||||
(async () => {
|
||||
let out = '';
|
||||
if (cmd === 'detect') out = await detect();
|
||||
else if (cmd === 'answer') out = await answer(process.argv.slice(3).join(' '));
|
||||
else if (cmd === 'answer') {
|
||||
try { out = await answer(process.argv.slice(3).join(' ')); }
|
||||
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); }
|
||||
if (out) console.log(out);
|
||||
})().catch(e => { console.error('[concierge-live]', e.message); process.exit(1); });
|
||||
|
||||
Reference in New Issue
Block a user