From 2d2ced54a1d3e32cdc1e7a1b7498bd3bcdc8c297 Mon Sep 17 00:00:00 2001 From: Tallagrand Thierry Date: Sat, 19 Sep 2026 18:35:38 +0200 Subject: [PATCH] Detection anomalie 'branchee mais ne charge pas' (je detecte, SPARK envoie) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Grief du proprietaire du 19/09 : JEEP branchee, du soleil, borne figee apres un swap rapide -> rien qui charge, et il ne l'a su qu'en regardant. Le service d'historisation detecte desormais l'episode et le SIGNALE dans un helper ; la session OpenClaw lira ce helper et enverra l'alerte WhatsApp (son cote). Critere, robuste par la censure sans injection : batterie pleine => MPPT bride => PV ~ conso => excedent ~ 0 => PAS d'alerte. Un excedent POSITIF qui persiste (> 500 W) avec vehicule branche + borne < 300 W + etat controleur bloquant, tenu > 10 min => il manque un preneur a de l'energie gratuite => signalement. Un seul par episode (dedup sur son debut), helper efface a la resolution. - helper input_text.evcs_alerte_charge (max 255, sans initial:), reload fait. - detecterAnomalieCharge() + ecrireHelperTexte(), appele dans cycle(). - etat d'episode en processus (repart de null au redemarrage, se re-arme en 10 min sur une anomalie reelle). ⚠️ SEUILS A CALIBRER sur l'historique avant d'etre figes. Le helper reste INERTE tant que SPARK n'a pas cable l'envoi : deployer maintenant ne fait rien partir. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj --- tools/historise-evcs.mjs | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/tools/historise-evcs.mjs b/tools/historise-evcs.mjs index eb2fff3..b062c50 100644 --- a/tools/historise-evcs.mjs +++ b/tools/historise-evcs.mjs @@ -1090,6 +1090,14 @@ const CANAUX_HUMAINS = new Set(['whatsapp', 'proprietaire']); const LABELS = new Set(['VW', 'JEEP', 'MEGANE', 'INVITE', 'ZERO']); let dernierBlancVu = null; // anti-repetition du journal en essai a blanc +// Detection d'anomalie "branchee mais ne charge pas" (19/09/2026). Etat +// d'episode EN PROCESSUS : instant ou la condition a commence a tenir, null +// sinon. Sur un redemarrage il repart de null — une anomalie reelle et +// persistante se re-arme en ANOMALIE_CHARGE_MIN. On garde aussi l'horodatage +// du dernier episode SIGNALE pour ne journaliser qu'une fois par episode. +let anomalieChargeDepuis = null; +let anomalieChargeSignalee = null; + // --------------------------------------------------------------------- // LA REPONSE DU PROPRIETAIRE PILOTE LE SELECTEUR // --------------------------------------------------------------------- @@ -1691,6 +1699,92 @@ async function appliquerOrdreAirtria(sql, client, st) { return 1; } +// --------------------------------------------------------------------- +// ANOMALIE "branchee mais ne charge pas" (19/09/2026) +// --------------------------------------------------------------------- +// Grief du proprietaire : la JEEP branchee, du soleil dispo, et rien qui +// charge (borne figee apres un swap trop rapide). Je DETECTE, la session +// OpenClaw (SPARK) ENVOIE l'alerte WhatsApp en lisant le helper. +// +// Critere, et pourquoi il ne se declenche PAS sur une voiture vraiment pleine : +// sans injection reseau, une batterie pleine fait BRIDER le MPPT (censure) donc +// PV ~ conso donc excedent ~ 0 -> pas d'alerte. Un excedent POSITIF qui PERSISTE +// avec voiture branchee et borne a ~0 W signifie qu'il manque un preneur a de +// l'energie gratuite : ca, ca vaut un signalement. +// - vehicule branche +// - borne ac_power < SEUIL_CHARGE_W (ne charge pas) +// - etat controleur bloquant (source_mode "Bloque" OU surplus_state d'attente) +// - excedent solaire (pv - conso) > ANOMALIE_CHARGE_SURPLUS_W +// - tenu >= ANOMALIE_CHARGE_MIN +// ⚠️ SEUILS A CALIBRER sur l'historique avant d'etre figes (lecon des +// oscillateurs) — valeurs de depart conservatrices. Un seul signalement par +// episode. Le helper reste INERTE tant que SPARK n'a pas cable l'envoi. +const ANOMALIE_CHARGE_MIN = 10; // minutes +const ANOMALIE_CHARGE_SURPLUS_W = 500; // excedent solaire minimal +const ETATS_BLOQUANTS = new Set([ + 'day_vehicle_full_idle', 'idle_wait_conditions', 'idle_wait_source', + 'stop_source_lost', 'night_session_done_idle', 'night_recovery_done_idle', +]); + +async function detecterAnomalieCharge(sql, st) { + const g = (id) => st.get(id); + const connecte = txt(g('binary_sensor.victron_evcs_vehicle_connected')?.state) === 'on'; + const acPower = num(g('sensor.victron_evcs_ac_power')?.state) ?? 0; + const pv = num(g('sensor.victron_pv_power_system')?.state) ?? 0; + const conso = num(g('sensor.victron_consumption_power_l1')?.state) ?? 0; + const excedent = pv - conso; // sans injection, ~ puissance batterie au signe pres + const sourceMode = txt(g('input_text.victron_evcs_source_mode')?.state) || ''; + const surplusState = (txt(g('input_text.victron_evcs_surplus_state')?.state) || '').toLowerCase(); + const bloquant = sourceMode.startsWith('Bloqu') || ETATS_BLOQUANTS.has(surplusState); + + const condition = connecte && acPower < SEUIL_CHARGE_W && bloquant + && excedent > ANOMALIE_CHARGE_SURPLUS_W; + + const now = Date.now(); + if (!condition) { + // fin d'episode : on efface le helper si un episode etait actif + if (anomalieChargeDepuis !== null) { + anomalieChargeDepuis = null; + await ecrireHelperTexte('input_text.evcs_alerte_charge', ''); + } + return 0; + } + + if (anomalieChargeDepuis === null) anomalieChargeDepuis = now; + const tenuMin = (now - anomalieChargeDepuis) / 60000; + if (tenuMin < ANOMALIE_CHARGE_MIN) return 0; + + // Episode confirme. Un seul signalement par episode (clef = son debut). + if (anomalieChargeSignalee === anomalieChargeDepuis) return 0; + anomalieChargeSignalee = anomalieChargeDepuis; + + const since = new Date(anomalieChargeDepuis).toISOString(); + const message = `Vehicule branche depuis ${Math.round(tenuMin)} min, ` + + `${Math.round(excedent)} W de soleil dispo, mais la borne ne charge pas ` + + `(${sourceMode || surplusState}). Verifier la borne / rebrancher.`; + await ecrireHelperTexte('input_text.evcs_alerte_charge', + JSON.stringify({ active: true, since, excedent_w: Math.round(excedent), + source_mode: sourceMode, surplus_state: surplusState, message })); + await journaliser(sql, 'anomalie', + `Branchee mais ne charge pas : ${Math.round(excedent)} W de soleil perdu depuis ${Math.round(tenuMin)} min`, + { since, excedent_w: Math.round(excedent), ac_power_w: acPower, + source_mode: sourceMode, surplus_state: surplusState }); + return 1; +} + +// Petit ecrivain de helper texte, reutilise par la detection d'anomalie. +async function ecrireHelperTexte(entite, valeur) { + if (DRY) return; + try { + await fetch(`${HA_URL}/api/services/input_text/set_value`, { + method: 'POST', + headers: { Authorization: `Bearer ${HA_TOKEN}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ entity_id: entite, value: valeur }), + signal: AbortSignal.timeout(8000), + }); + } catch { /* le helper reste a sa valeur, on reessaiera au prochain cycle */ } +} + async function cycle(client) { // En blanc, on compte ce qu'on ECRIRAIT. Un essai a blanc qui renvoie // toujours zero ne verifie rien — c'est le piege classique du test qui ne @@ -1712,6 +1806,9 @@ async function cycle(client) { prevision: await ecrirePrevision(sql, st), session: await ecrireSession(sql, client, st), evenement: await detecterEvenements(sql, st), + anomalie: await detecterAnomalieCharge(sql, st).catch((e) => { + console.error(`detection anomalie charge en echec : ${e.message}`); return 0; + }), etiquette: await appliquerEtiquetteConcierge(sql, client, st).catch((e) => { console.error(`etiquette concierge en echec : ${e.message}`); return 0; }) + await appliquerEtiquettesEnAttente(sql, client).catch((e) => {