Bilan : la nuit prevue suit la soiree reelle (regression sur 24 nuits), nuage Grafana corrige

Le service publie le facteur nuit prevue / nuit moyenne (sensor.evcs_nuit_maison_prevue_kwh),
le bilan l'applique a son profil de 21:30 a 09:00. Diagnostics 81/81, bilan 68/68.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uJ47NxgfpGnuuMr3f2n7Z
This commit is contained in:
Tallagrand Thierry
2026-09-28 23:36:08 +02:00
parent fc5b219abf
commit e18158bfd1
14 changed files with 306 additions and 153 deletions

View File

@@ -677,3 +677,70 @@ export function vrmVersCourbes(paires, maintenant = new Date()) {
updated: `${p2(d.getDate())}/${p2(d.getMonth() + 1)} ${p2(d.getHours())}:${p2(d.getMinutes())}`,
};
}
// ---------------------------------------------------------------------
// La nuit de la maison, prevue par la soiree (28/09/2026)
// ---------------------------------------------------------------------
// Demande du proprietaire : « si tu superposes 20 nuits tu dois trouver un
// nuage de points ». Sur 24 nuits (04/09 -> 27/09), l'energie consommee par la
// maison de 21:30 a 09:00 (voiture exclue) suit celle de la soiree 18:00-21:30 :
// nuit = 6,98 + 0,74 x soiree, r2 = 0,40. Une soiree chargee (four) annonce une
// nuit chargee ; le profil horaire MOYEN du bilan ne le voit pas.
//
// Ce qui est publie n'est PAS un kWh a substituer au bilan : c'est un FACTEUR,
// nuit prevue / nuit moyenne des MEMES donnees. Le profil du bilan vient des
// compteurs d'energie, la regression des puissances integrees : le rapport
// annule l'ecart de methode, que la difference ne ferait qu'ajouter.
//
// Garde-fous : au moins 15 nuits et r2 >= 0,2, sinon le modele ne vaut rien et
// on le dit (`indisponible`). Hors 21:30 -> 09:00, pas de soiree complete a
// lire : `hors_fenetre`.
export const NUIT_MIN_NUITS = 15;
export const NUIT_MIN_R2 = 0.2;
function heureLocaleMs(jour, hhmm, reference) {
// Meme calcul de decalage que minuitLocal, pour une heure de Paris donnee.
const decalage = new Date(reference.toLocaleString('en-US', { timeZone: 'Europe/Paris' })) -
new Date(reference.toLocaleString('en-US', { timeZone: 'UTC' }));
return Date.parse(`${jour}T${hhmm}:00Z`) - decalage;
}
export function nuitMaisonPrevue(nuits, soireeKwh, maintenant = new Date()) {
const jourDe = (d) => new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Paris' }).format(d);
const hm = Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit', hour12: false })
.format(maintenant).replace(':', ''));
const aujourdhui = jourDe(maintenant);
const hier = jourDe(new Date(maintenant.getTime() - 86400000));
const demain = jourDe(new Date(maintenant.getTime() + 86400000));
const fenetre = hm >= 2130 ? { soir: aujourdhui, matin: demain } : hm < 900 ? { soir: hier, matin: aujourdhui } : null;
const pts = (nuits || []).map((x) => [Number(x.soiree), Number(x.nuit)]).filter(([a, b]) => Number.isFinite(a) && Number.isFinite(b));
const n = pts.length;
let modele = null;
if (n >= 2) {
const mx = pts.reduce((s, p) => s + p[0], 0) / n, my = pts.reduce((s, p) => s + p[1], 0) / n;
let sxy = 0, sxx = 0, syy = 0;
for (const [x, y] of pts) { sxy += (x - mx) * (y - my); sxx += (x - mx) ** 2; syy += (y - my) ** 2; }
if (sxx > 0 && syy > 0) {
const b = sxy / sxx;
modele = { a: my - b * mx, b, r2: (sxy * sxy) / (sxx * syy), moyenne: my };
}
}
const r2 = (v) => Math.round(v * 100) / 100;
const attributs = {
nuits: n, soiree_kwh: Number.isFinite(soireeKwh) ? r2(soireeKwh) : null,
a: modele ? r2(modele.a) : null, b: modele ? Math.round(modele.b * 1000) / 1000 : null,
r2: modele ? r2(modele.r2) : null, nuit_moyenne_kwh: modele ? r2(modele.moyenne) : null,
debut_ms: fenetre ? heureLocaleMs(fenetre.soir, '21:30', maintenant) : null,
fin_ms: fenetre ? heureLocaleMs(fenetre.matin, '09:00', maintenant) : null,
};
if (!fenetre) return { etat: 'hors_fenetre', attributs: { ...attributs, facteur: null, statut: 'hors_fenetre' } };
if (!modele || n < NUIT_MIN_NUITS || modele.r2 < NUIT_MIN_R2 || !Number.isFinite(soireeKwh)) {
return { etat: 'indisponible', attributs: { ...attributs, facteur: null, statut: 'indisponible',
explication: !Number.isFinite(soireeKwh) ? 'soiree illisible'
: n < NUIT_MIN_NUITS ? `seulement ${n} nuits (${NUIT_MIN_NUITS} requises)`
: `modele trop faible (r2 ${modele ? r2(modele.r2) : '?'} < ${NUIT_MIN_R2})` } };
}
const prevue = modele.a + modele.b * soireeKwh;
return { etat: r2(prevue), attributs: { ...attributs, facteur: Math.round((prevue / modele.moyenne) * 1000) / 1000,
statut: 'ok', explication: `soiree ${r2(soireeKwh)} kWh -> nuit prevue ${r2(prevue)} kWh (moyenne ${r2(modele.moyenne)}, ${n} nuits, r2 ${r2(modele.r2)})` } };
}