Revue glissante toutes les 15 min (alertes), recharge maximale jugee contre le possible

This commit is contained in:
Tallagrand Thierry
2026-10-10 15:07:37 +02:00
parent b9bdb44f4c
commit 3988a0a7f5
4 changed files with 146 additions and 10 deletions

View File

@@ -2361,5 +2361,21 @@ batterie a 32 A et JEEP (branchee 13:01, 52 %) qui ne demarre pas. SOC 47 % a 12
- 14:05 : nouveau branchement (1,8 kW), etiquete VW par defaut et non confirme : probablement la JEEP (18 % a 13:16,
depart 15:30) — a confirmer par le proprietaire.
### 10/10 ~15:30 — Revue glissante (accord du proprietaire)
- JEEP du 14:05 confirmee par le proprietaire (concierge 15:00). Pas d'identification automatique : garee a 13:00,
branchee a 14:05 (> 10 min apres la fin de trajet) et aucune publication Uconnect au demarrage de la charge (voiture
endormie ; la reveiller serait une commande, interdite). Le concierge a pose la question : comportement voulu.
- **Revue : recharge maximale jugee contre le POSSIBLE** (`rendementChargeMax`, tools/revue-jour.mjs) : possible =
somme minute par minute de min(7,3 kW, 90 % de l'abonnement - maison) x 0,92, sans le solaire (estimation basse) ;
gain < 50 % -> constat `charge_max_bridee` « a examiner » (sinon `charge_max_midi` info). Le 07/10 (31 -> 35 %,
+1,2 kWh pour ~13 kWh) devient « bridee ». Constat EN COURS `charge_max_bridee_en_cours` apres 60 min de fenetre
(`d.enCours`). Tests revue 17/17.
- **Revue glissante** (service, `revueGlissante`, toutes les 15 min a la minute 11, fenetre 3 h) : constats « a
examiner » nouveaux (`edf_heures_pleines` >= 15 min, `charge_max_bridee[_en_cours]`, `batterie_sous_objectif_*` ;
la voiture bloquee reste a l'arbitre) -> table `revue_alerte` (un episode qui dure prolonge sa ligne) et
`sensor.evcs_alerte_revue` (etat = id ; attributs code, texte, etat_systeme, a, dernieres). SPARK relaie.
Rejeu : 09/10 13:30 -> « recharge maximale BRIDEE : +1,8 kWh pour ~7,3 kWh permis » ; 08/10 00:30 -> EDF en HP ;
10/10 15:30 -> rien. Service redemarre ~15:30.
---
*Document mis a jour le : 10/10/2026 (~14:15) — conseiller relecteur de la journee (revue calculee + pistes) ; nuit : voiture qui profite d'EDF ferme pour la batterie, nuit qui prepare le soir les jours annonces faibles ; 08/10/2026 (~00:15) — jour faible : charge maximale en HC de midi (bilan 100 %, ACM plafond + solaire) ; 07/10/2026 (~22:45) — reprise de 16 h verifiee, EDF en HP a 18:03, bilan : reprise du soir et prevision recalee sur la matinee ; bilan HC de midi, reprise du soir apres coupure en pleine chauffe, mesure ECS sur 10 min ; recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*
*Document mis a jour le : 10/10/2026 (~15:35) — conseiller relecteur de la journee (revue calculee + pistes) ; nuit : voiture qui profite d'EDF ferme pour la batterie, nuit qui prepare le soir les jours annonces faibles ; 08/10/2026 (~00:15) — jour faible : charge maximale en HC de midi (bilan 100 %, ACM plafond + solaire) ; 07/10/2026 (~22:45) — reprise de 16 h verifiee, EDF en HP a 18:03, bilan : reprise du soir et prevision recalee sur la matinee ; bilan HC de midi, reprise du soir apres coupure en pleine chauffe, mesure ECS sur 10 min ; recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*

View File

@@ -75,8 +75,9 @@ const sessionMidi = (d, requis) => {
const c = constatsRevue(d).constats.find((x) => x.code === 'batterie_sous_objectif_midi');
ok('midi-sous-objectif-0710', !!c && /35 % pour 43,6 % vises, il manquait 2,6 kWh/.test(c.texte), JSON.stringify(c)); }
{ const d = base(); sessionMidi(d, 100);
const r = constatsRevue(d); const c = r.constats.find((x) => x.code === 'charge_max_midi');
ok('midi-charge-max-info', !!c && c.gravite === 'info' && /31 % -> 35 % \(\+1,2 kWh\)/.test(c.texte) && !codes(r).includes('sous_objectif'), JSON.stringify(r.constats)); }
const r = constatsRevue(d); const c = r.constats.find((x) => /charge_max/.test(x.code));
// 10/10 : 31 -> 35 % en 2 h (+1,2 kWh) quand l'abonnement en permettait ~13 kWh = le 07/10, recharge BRIDEE.
ok('midi-charge-max-bridee-0710', !!c && c.code === 'charge_max_bridee' && c.gravite === 'a_examiner' && /31 % -> 35 % \(\+1,2 kWh/.test(c.texte) && !codes(r).includes('sous_objectif'), JSON.stringify(r.constats)); }
{ const d = base(); sessionMidi(d, 43.6);
d.acmA = [{ ts: T('07T12:00:00'), v: 30 }]; d.consigneW = [{ ts: T('07T12:30:00'), v: 5198 }];
@@ -85,5 +86,24 @@ const sessionMidi = (d, requis) => {
ok('etat-joint-au-constat', !!c && /a 13:24/.test(c.etat) && /ACM 30 A/.test(c.etat) && /consigne reseau 5198 W/.test(c.etat)
&& /mode de nuit sans_edf/.test(c.etat) && /session HC engagee oui/.test(c.etat) && /budget voiture sans EDF inconnu/.test(c.etat), c && c.etat); }
// 10/10 : recharge maximale jugee contre le possible. Releve du 09/10 : maison ~2,2 kW,
// 49 -> 62 % (+3,9 kWh) sur 2 h ; possible ~ min(7,3 ; 10,8 - 2,2) x 0,92 x 2 = 13,4 kWh.
{ const d = base(); sessionMidi(d, 100);
d.soc = [{ ts: T('07T12:20:00'), v: 49 }, { ts: T('07T14:20:00'), v: 62 }];
d.maisonW = [{ ts: T('07T12:00:00'), v: 2200 }];
const c = constatsRevue(d).constats.find((x) => /charge_max/.test(x.code));
ok('charge-max-bridee-a-examiner', c && c.code === 'charge_max_bridee' && c.gravite === 'a_examiner' && /13,4 kWh permis/.test(c.texte), c && c.texte); }
{ const d = base(); sessionMidi(d, 100);
d.soc = [{ ts: T('07T12:20:00'), v: 40 }, { ts: T('07T14:20:00'), v: 75 }];
d.maisonW = [{ ts: T('07T12:00:00'), v: 2200 }];
const c = constatsRevue(d).constats.find((x) => /charge_max/.test(x.code));
ok('charge-max-normale-info', c && c.code === 'charge_max_midi' && c.gravite === 'info', c && c.texte); }
{ const d = base(); sessionMidi(d, 100);
d.soc = [{ ts: T('07T12:20:00'), v: 49 }, { ts: T('07T13:30:00'), v: 52 }];
d.maisonW = [{ ts: T('07T12:00:00'), v: 2200 }];
d.debut = T('07T11:30:00'); d.fin = T('07T13:30:00'); d.enCours = true;
const c = constatsRevue(d).constats.find((x) => x.code === 'charge_max_bridee_en_cours');
ok('charge-max-bridee-en-cours', c && /depuis 12:24/.test(c.texte) && c.gravite === 'a_examiner', JSON.stringify(constatsRevue(d).constats.map((x) => x.code))); }
console.log(echecs ? `\n${echecs} ECHEC(S)` : '\nREVUE OK');
process.exit(echecs ? 1 : 0);

View File

@@ -1280,6 +1280,55 @@ async function publierRevueHistorique(client) {
return 1;
}
// ---------------------------------------------------------------------
// REVUE GLISSANTE (10/10/2026, proprietaire) : les memes constats que la revue
// du matin, toutes les 15 min sur les 3 dernieres heures. Un constat « a
// examiner » nouveau devient une ALERTE : sensor.evcs_alerte_revue (etat = id),
// relayee par SPARK sur WhatsApp, historisee dans revue_alerte. La voiture
// bloquee reste a l'arbitre (pas de doublon). Un episode qui dure (EDF en heures
// pleines) ne donne qu'une alerte : on prolonge la ligne existante.
const CODES_GLISSANTS = new Set(['edf_heures_pleines', 'charge_max_bridee_en_cours', 'charge_max_bridee',
'batterie_sous_objectif_nuit', 'batterie_sous_objectif_midi']);
let revueGlissanteSchema = false;
async function revueGlissante(sql, client, st) {
if (!client) return 0;
if (!revueGlissanteSchema) {
await client.query(`CREATE TABLE IF NOT EXISTS revue_alerte (
id text PRIMARY KEY, code text, debut timestamptz, fin timestamptz, texte text, etat text, envoyee timestamptz DEFAULT now())`);
revueGlissanteSchema = true;
}
const hc = (id) => (st.get(id)?.state || '').slice(0, 5);
const fenetres = [
{ nom: 'nuit', debut: hc('input_datetime.victron_evcs_hc_night_start'), fin: hc('input_datetime.victron_evcs_hc_night_stop') },
{ nom: 'midi', debut: hc('input_datetime.victron_evcs_hc_midday_start'), fin: hc('input_datetime.victron_evcs_hc_midday_stop') },
];
const d = await donneesRevue(client, fenetres, Date.now(), 180);
d.enCours = true;
d.abonnementVa = Number(st.get('input_number.victron_grid_subscription_va')?.state) || 12000;
const { constats } = constatsRevue(d);
let n = 0;
for (const c of constats) {
if (c.gravite !== 'a_examiner' || !CODES_GLISSANTS.has(c.code)) continue;
if (c.code === 'edf_heures_pleines' && (c.chiffres?.minutes ?? 0) < 15) continue;
const { rows: [deja] } = await client.query(
`SELECT id FROM revue_alerte WHERE code = $1 AND fin >= $2 ORDER BY fin DESC LIMIT 1`,
[c.code, new Date(c.debut - 20 * 60000)]);
if (deja) { await client.query(`UPDATE revue_alerte SET fin = $2, texte = $3 WHERE id = $1`, [deja.id, new Date(c.fin), c.texte]); continue; }
const id = `${c.code}-${new Date(c.debut).toISOString().slice(0, 16)}`;
await client.query(`INSERT INTO revue_alerte (id, code, debut, fin, texte, etat) VALUES ($1,$2,$3,$4,$5,$6) ON CONFLICT DO NOTHING`,
[id, c.code, new Date(c.debut), new Date(c.fin), c.texte, c.etat || null]);
const { rows: hist } = await client.query(`SELECT id, code, texte, to_char(envoyee AT TIME ZONE 'Europe/Paris','DD/MM HH24:MI') a
FROM revue_alerte ORDER BY envoyee DESC LIMIT 10`);
await publierCapteur('sensor.evcs_alerte_revue', id, {
friendly_name: 'Alerte de la revue glissante', icon: 'mdi:alert-outline',
id, code: c.code, texte: c.texte, etat_systeme: c.etat || '', a: new Date().toISOString(), dernieres: hist,
});
await journaliser(sql, 'revue', `Alerte revue glissante : ${c.code}`, { id, texte: c.texte });
n++;
}
return n;
}
function decalageParis(jour) {
const d = new Date(`${jour}T12:00:00Z`);
const h = Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', hour12: false }).format(d));
@@ -2669,6 +2718,9 @@ async function cycle(client) {
arbitrage: DRY ? 0 : await arbitrer(sql, client, st).catch((e) => {
console.error('arbitrage :', e.message); return 0;
}),
revueGlissante: (DRY || new Date().getMinutes() % 15 !== 11) ? 0 : await revueGlissante(sql, client, st).catch((e) => {
console.error('revue glissante :', e.message); return 0;
}),
revue: DRY ? 0 : await revueDeLaJournee(sql, client, st).catch((e) => {
console.error('revue de la journee :', e.message); return 0;
}),

View File

@@ -22,6 +22,27 @@ const SEUIL_RESEAU_W = 100; // bruit de mesure du reseau (comme sensor.
const SEUIL_VOITURE_MIN = 10; // voiture bloquee pendant une session HC
const SEUIL_OBJECTIF_PTS = 2; // batterie sous son objectif en fin de HC
const CHARGEURS_MAX_W = 7300; // 2 MultiPlus, 140 A (HANDOFF 07/10)
const RENDEMENT_CHARGE = 0.92;
const PART_ABONNEMENT = 0.9; // marge sous l'abonnement, comme l'ACM
// Recharge maximale de midi (jour faible) : gain reel contre ce que l'abonnement
// permettait, minute par minute (90 % de l'abonnement - maison, borne aux
// chargeurs, rendement), sans compter le solaire (estimation basse du possible).
export function rendementChargeMax(d, t0, t1) {
const s0 = aInstant(d.soc, t0), s1 = aInstant(d.soc, t1);
if (!Number.isFinite(s0) || !Number.isFinite(s1) || t1 <= t0) return null;
const abo = Number(d.abonnementVa) || 12000;
let possibleKwh = 0;
for (let t = t0; t < t1; t += MIN) {
const maison = d.maisonW ? aInstant(d.maisonW, t) : null;
const libre = Math.max(0, PART_ABONNEMENT * abo - (Number.isFinite(maison) ? maison : 0));
possibleKwh += Math.min(CHARGEURS_MAX_W, libre) * RENDEMENT_CHARGE / 60000;
}
const gainKwh = (s1 - s0) * KWH_PAR_POINT;
return { s0, s1, gainKwh, possibleKwh, bridee: possibleKwh > 1 && gainKwh < 0.5 * possibleKwh };
}
const ETATS_VOITURE_ABSENTE_OU_PLEINE = /not_connected|done|full|complete|charged/i;
function minutesParis(ms) {
@@ -178,13 +199,17 @@ export function constatsRevue(d) {
let t0 = null;
for (let u = t - 4 * 60 * MIN; u < t; u += MIN) if (minutesParis(u) === a) { t0 = u; break; }
const soc0 = t0 === null ? null : aInstant(d.soc, t0);
// 10/10 : le gain se juge contre le POSSIBLE. Le 09/10, +3,9 kWh en 2 h pour
// ~8-10 kWh permis : la revue ne disait que « info ».
const r = t0 === null ? null : rendementChargeMax(d, t0, t);
constats.push({
code: 'charge_max_midi',
gravite: 'info',
code: r && r.bridee ? 'charge_max_bridee' : 'charge_max_midi',
gravite: r && r.bridee ? 'a_examiner' : 'info',
debut: t, fin: t,
texte: `Heures creuses de midi en charge maximale (jour faible) : batterie ${soc0 === null ? '?' : Math.round(soc0)} % -> ${Math.round(soc)} %`
+ (soc0 === null ? '.' : ` (+${f1((soc - soc0) * KWH_PAR_POINT)} kWh).`),
chiffres: { socDebut: soc0, socFin: soc },
+ (soc0 === null ? '.' : ` (+${f1((soc - soc0) * KWH_PAR_POINT)} kWh`)
+ (r ? ` pour environ ${f1(r.possibleKwh)} kWh permis par l'abonnement${r.bridee ? ', moins de la moitie : recharge BRIDEE' : ''}).` : (soc0 === null ? '' : ').')),
chiffres: { socDebut: soc0, socFin: soc, possibleKwh: r ? Math.round(r.possibleKwh * 10) / 10 : null },
etat: etatA(d, t - 60 * MIN),
contexte: (x.raison || '').slice(0, 400),
});
@@ -205,6 +230,28 @@ export function constatsRevue(d) {
}
}
// 4. EN COURS (revue glissante, 10/10) : recharge maximale de midi bridee, jugee
// apres au moins 60 min de fenetre (le constat de fin de fenetre arrive trop tard).
if (d.enCours) {
const midi = d.fenetres.find((f) => f.nom === 'midi');
const a = midi ? hm(midi.debut) : null, b = midi ? hm(midi.fin) : null;
const m = minutesParis(d.fin);
const x = dec(d.fin);
const requis = aInstant(d.requis, d.fin);
if (a !== null && b !== null && m >= a + 60 && m < b && x && x.session_engagee === true && requis >= 99.5) {
const t0 = d.fin - (m - a) * MIN;
const r = rendementChargeMax(d, t0, d.fin);
if (r && r.bridee) {
constats.push({
code: 'charge_max_bridee_en_cours', gravite: 'a_examiner', debut: t0, fin: d.fin,
texte: `Recharge maximale de midi BRIDEE : depuis ${hhmm(t0)}, batterie ${Math.round(r.s0)} % -> ${Math.round(r.s1)} % (+${f1(r.gainKwh)} kWh) pour environ ${f1(r.possibleKwh)} kWh permis par l'abonnement.`,
chiffres: { gainKwh: Math.round(r.gainKwh * 10) / 10, possibleKwh: Math.round(r.possibleKwh * 10) / 10 },
etat: etatA(d, d.fin), contexte: (x.raison || '').slice(0, 400),
});
}
}
}
constats.sort((a, b) => a.debut - b.debut);
const texte = constats.length
? constats.map((c) => `- ${c.texte}`).join('\n')
@@ -212,10 +259,11 @@ export function constatsRevue(d) {
return { constats, texte };
}
export async function donneesRevue(client, fenetres, finMs) {
// 24 h qui se terminent a la fin de la fenetre HC de nuit (07:24 + 1 min).
export async function donneesRevue(client, fenetres, finMs, dureeMin = 24 * 60) {
// Par defaut 24 h qui se terminent a la fin de la fenetre HC de nuit (07:24 +
// 1 min) ; la revue glissante (10/10) regarde les 2 ou 3 dernieres heures.
const fin = finMs;
const debut = fin - 24 * 60 * MIN;
const debut = fin - dureeMin * MIN;
const q = async (sql, v) => (await client.query(sql, v)).rows;
const d0 = new Date(debut), d1 = new Date(fin);
const ms = (r) => new Date(r.ts).getTime();