Detection anomalie 'branchee mais ne charge pas' (je detecte, SPARK envoie)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
@@ -1090,6 +1090,14 @@ const CANAUX_HUMAINS = new Set(['whatsapp', 'proprietaire']);
|
|||||||
const LABELS = new Set(['VW', 'JEEP', 'MEGANE', 'INVITE', 'ZERO']);
|
const LABELS = new Set(['VW', 'JEEP', 'MEGANE', 'INVITE', 'ZERO']);
|
||||||
let dernierBlancVu = null; // anti-repetition du journal en essai a blanc
|
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
|
// LA REPONSE DU PROPRIETAIRE PILOTE LE SELECTEUR
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
@@ -1691,6 +1699,92 @@ async function appliquerOrdreAirtria(sql, client, st) {
|
|||||||
return 1;
|
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) {
|
async function cycle(client) {
|
||||||
// En blanc, on compte ce qu'on ECRIRAIT. Un essai a blanc qui renvoie
|
// 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
|
// 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),
|
prevision: await ecrirePrevision(sql, st),
|
||||||
session: await ecrireSession(sql, client, st),
|
session: await ecrireSession(sql, client, st),
|
||||||
evenement: await detecterEvenements(sql, 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) => {
|
etiquette: await appliquerEtiquetteConcierge(sql, client, st).catch((e) => {
|
||||||
console.error(`etiquette concierge en echec : ${e.message}`); return 0;
|
console.error(`etiquette concierge en echec : ${e.message}`); return 0;
|
||||||
}) + await appliquerEtiquettesEnAttente(sql, client).catch((e) => {
|
}) + await appliquerEtiquettesEnAttente(sql, client).catch((e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user