concierge et fiche : regles Jeep rapides, decouverte tardive, alerte chauffe-eau
- concierge-live.js : question immediate si la Jeep a le contact mis (info fraiche) ou un GPS frais hors de zone.maison_jeep ; pas de question pour un branchement decouvert plus de 2 h apres son heure (trace) ; relais de l'alerte chauffe-eau (input_text.evcs_alerte_chauffe_eau, une fois par fenetre HC, active === true). - AGENTS.md : chauffe-eau en lecture seule (etat reel = switch, raison, pas de pilotage, BOOST sur l'appareil), jour faible = EDF aux HC de nuit et de midi, recharge HC expliquee par victron_charge_reason. - Journaux de tri Orange et AIRTRIA, DREAMS.
This commit is contained in:
@@ -53,6 +53,11 @@ const ALERT_HELPER = 'input_text.evcs_alerte_charge';
|
||||
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
|
||||
// 07/10/2026, demande explicite du propriétaire (via EVCS) : alerte chauffe-eau en heures creuses EDF quand il
|
||||
// ne tire que la pompe à chaleur. Le workflow ECS pose le helper ; on relaie `message` UNE fois par fenêtre
|
||||
// (dedup sur `since`). Pas d'interrupteur d'opt-in ; même coupure d'urgence locale que l'alerte de charge.
|
||||
const ECS_HELPER = 'input_text.evcs_alerte_chauffe_eau';
|
||||
const ECS_STATE = path.join(__dirname, 'concierge-alerte-ecs-state.json');
|
||||
const WINDOW_DAYS = 5;
|
||||
const FRESH_BEFORE_S = 600, FRESH_AFTER_S = 1800;
|
||||
|
||||
@@ -85,9 +90,14 @@ async function haState(entity) {
|
||||
if (entity === W.HELPER_ENTITY) return { state: SIM.lastLabel || '' };
|
||||
if (entity === W.RELEVE_ENTITY) return { state: SIM.releves || '[]' };
|
||||
if (entity === W.LEVEL_ENTITY) return { state: String(SIM.lastLevel?.value ?? ''), last_reported: SIM.lastLevel?.at };
|
||||
if (entity === ECS_HELPER) return { state: SIM.ecs === undefined ? '' : (typeof SIM.ecs === 'string' ? SIM.ecs : JSON.stringify(SIM.ecs)) };
|
||||
if (entity === JEEP_CHARGER) return { state: SIM.jeep?.charger ?? 'unknown' };
|
||||
if (entity === JEEP_UPDATE) return { state: SIM.jeep?.update ?? 'unknown' };
|
||||
if (entity === JEEP_HV) return { state: String(SIM.jeep?.hv ?? 'unknown') };
|
||||
if (entity === JEEP_IGN) return { state: SIM.jeep?.ignition ?? 'off' };
|
||||
if (entity === JEEP_LOC) return { state: 'x', attributes: { latitude: SIM.jeep?.lat ?? 48.7903, longitude: SIM.jeep?.lon ?? -3.5327 } };
|
||||
if (entity === JEEP_LOC_AT) return { state: SIM.jeep?.locAt ?? '1970-01-01T00:00:00+00:00' };
|
||||
if (entity === ZONE_JEEP) return { state: '0', attributes: { latitude: 48.7903, longitude: -3.5327, radius: 150 } };
|
||||
if (entity === JEEP_ODO) return { state: '112787', last_changed: SIM.jeep?.odoChanged ?? '1970-01-01T00:00:00+00:00' };
|
||||
return { state: '', attributes: { options: SIM.options || ['VW', 'JEEP', 'ZERO'] } };
|
||||
}
|
||||
@@ -143,11 +153,29 @@ const JEEP_CHARGER = 'binary_sensor.jeep_renegade_phev_ev_charger';
|
||||
const JEEP_UPDATE = 'sensor.jeep_renegade_phev_last_info_update_at';
|
||||
const JEEP_HV = 'sensor.jeep_renegade_phev_hvbattery_charge';
|
||||
const JEEP_ODO = 'sensor.jeep_renegade_phev_odometer'; // last_changed ne bouge que si le compteur change
|
||||
const JEEP_IGN = 'binary_sensor.jeep_renegade_phev_ignition';
|
||||
// 07/10/2026 (accord du propriétaire) : zone de stationnement de la Jeep, créée par EVCS. Distance calculée ici
|
||||
// (le libellé d'état du tracker n'est pas fiable) ; la position ne compte que si son horodatage GPS est frais.
|
||||
const JEEP_LOC = 'device_tracker.jeep_renegade_phev_location';
|
||||
const JEEP_LOC_AT = 'sensor.jeep_renegade_phev_last_location_update_at';
|
||||
const ZONE_JEEP = 'zone.maison_jeep';
|
||||
function distanceM(a1, o1, a2, o2) {
|
||||
const R = 6371000, r = Math.PI / 180, dA = (a2 - a1) * r, dO = (o2 - o1) * r;
|
||||
const h = Math.sin(dA / 2) ** 2 + Math.cos(a1 * r) * Math.cos(a2 * r) * Math.sin(dO / 2) ** 2;
|
||||
return 2 * R * Math.asin(Math.sqrt(h));
|
||||
}
|
||||
const ATTENTE_UCONNECT_S = 900; // avant de questionner une voiture inconnue, laisser 15 min à Uconnect
|
||||
// 07/10/2026 (accord du propriétaire) : un branchement DÉCOUVERT plus de 2 h après son heure (aller-retour
|
||||
// bref rattrapé après coup par EVCS dans evenement) ne déclenche plus de question : trace seulement.
|
||||
const DECOUVERTE_TARDIVE_S = 7200;
|
||||
const TRACES = path.join(__dirname, 'concierge-traces.log');
|
||||
const trace = (msg) => { if (SIM) { console.error('[SIM trace] ' + msg); return; }
|
||||
try { fs.appendFileSync(TRACES, `${new Date().toISOString()} ${msg}\n`); } catch { /* jamais bloquant */ } };
|
||||
|
||||
async function jeepVerdict(onEpoch) {
|
||||
// 'jeep' (identifiée, avec niveau) | 'pas_jeep' (lue NON branchée après le branchement) | 'inconnu'
|
||||
let charger, upd, hv, odo;
|
||||
let charger, upd, hv, odo, ign = 'unknown';
|
||||
try { ign = (await haState(JEEP_IGN)).state; } catch { /* facultatif */ }
|
||||
try {
|
||||
charger = (await haState(JEEP_CHARGER)).state;
|
||||
upd = Date.parse((await haState(JEEP_UPDATE)).state) / 1000;
|
||||
@@ -155,6 +183,20 @@ async function jeepVerdict(onEpoch) {
|
||||
odo = Date.parse((await haState(JEEP_ODO)).last_changed) / 1000;
|
||||
} catch { return { v: 'inconnu' }; }
|
||||
if (!Number.isFinite(upd)) return { v: 'inconnu' };
|
||||
// 07/10/2026 (règle rapide, validée avec EVCS) : contact mis avec une info Uconnect fraîche (≥ branchement
|
||||
// −10 min) = la Jeep roule, ce n'est pas elle sur la borne → question immédiate. La POSITION n'est pas utilisée :
|
||||
// la Jeep garée est à 2,2 km de zone.home, donc « not_home » en permanence.
|
||||
if (ign === 'on' && upd >= onEpoch - 600) return { v: 'pas_jeep' };
|
||||
// Position GPS FRAÎCHE (≥ branchement −10 min) hors de la zone de stationnement → elle est ailleurs.
|
||||
try {
|
||||
const locAt = Date.parse((await haState(JEEP_LOC_AT)).state) / 1000;
|
||||
if (Number.isFinite(locAt) && locAt >= onEpoch - 600) {
|
||||
const loc = (await haState(JEEP_LOC)).attributes || {}, z = (await haState(ZONE_JEEP)).attributes || {};
|
||||
if ([loc.latitude, loc.longitude, z.latitude, z.longitude].every(Number.isFinite)
|
||||
&& distanceM(loc.latitude, loc.longitude, z.latitude, z.longitude) > (z.radius || 150))
|
||||
return { v: 'pas_jeep' };
|
||||
}
|
||||
} catch { /* position illisible : on n'en tire rien */ }
|
||||
const niveau = Number.isInteger(hv) && hv >= 0 && hv <= 100 ? hv : null;
|
||||
if (charger === 'on' && upd >= onEpoch - 300 && upd <= onEpoch + 1800)
|
||||
return { v: 'jeep', niveau };
|
||||
@@ -196,6 +238,11 @@ async function runDetection() {
|
||||
const age = Date.now() / 1000 - latest.on;
|
||||
if (jv.v !== 'pas_jeep' && age < ATTENTE_UCONNECT_S) return ''; // peut-être la JEEP : on repasse au prochain cycle
|
||||
|
||||
if (age > DECOUVERTE_TARDIVE_S) { // la JEEP identifiée a déjà eu sa ligne d'info plus haut, même en retard
|
||||
saveJSON(STATE, { lastOn: latest.on });
|
||||
trace(`branchement ${latest.on_text} découvert ${Math.round(age / 60)} min après son heure : pas de question`);
|
||||
return '';
|
||||
}
|
||||
const fresh = selectorFresh(latest.on, selChanges);
|
||||
saveJSON(STATE, { lastOn: latest.on }); // traité : pas de re-question
|
||||
if (fresh) return ''; // sélecteur réglé au branchement → confiance
|
||||
@@ -219,6 +266,9 @@ async function detect() {
|
||||
const transition = SIM ? '' : await health.publish({ ok, detail });
|
||||
let alert = '';
|
||||
if (!SIM) { try { alert = await checkAlert(); } catch { /* l'alerte ne casse jamais le cycle */ } }
|
||||
let alerteEcs = '';
|
||||
try { alerteEcs = await checkAlerteECS(); } catch { /* idem : jamais bloquant */ }
|
||||
if (alerteEcs) alert = [alert, alerteEcs].filter(Boolean).join('\n\n');
|
||||
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
|
||||
}
|
||||
@@ -467,6 +517,19 @@ async function checkAlert() {
|
||||
return String(a.message).slice(0, 300);
|
||||
}
|
||||
|
||||
async function checkAlerteECS() {
|
||||
if (fs.existsSync(ALERT_KILL)) return '';
|
||||
let raw;
|
||||
try { raw = (await haState(ECS_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 = SIM ? (SIM.ecsState || {}) : loadJSON(ECS_STATE, {});
|
||||
if (st.lastSince === a.since) return ''; // déjà relayée pour cette fenêtre HC
|
||||
saveJSON(ECS_STATE, { lastSince: a.since });
|
||||
return String(a.message).slice(0, 300);
|
||||
}
|
||||
|
||||
const cmd = process.argv[2];
|
||||
(async () => {
|
||||
let out = '';
|
||||
|
||||
Reference in New Issue
Block a user