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

@@ -140,11 +140,57 @@ if (missing.length) {
// - profil PRUDENT (p80) → planification : cible de nuit, dimensionnement
// du stockage, choix qui engagent plusieurs heures.
// ---------------------------------------------------------------------
function buildSlots(consumption) {
// ---------------------------------------------------------------------
// LA NUIT PREVUE PAR LA SOIREE (28/09/2026, demande du proprietaire)
//
// Le profil p50 est une mediane HORAIRE : il ignore la soiree qui vient de se
// passer. Or sur 24 nuits, la maison consomme d'autant plus de 21:30 a 09:00
// que la soiree 18:00-21:30 a ete chargee (four) : nuit = 6,98 + 0,74 x soiree,
// r2 = 0,40. Le service d'historisation publie le FACTEUR nuit prevue / nuit
// moyenne (sensor.evcs_nuit_maison_prevue_kwh) ; on multiplie le profil par ce
// facteur sur la seule fenetre 21:30 -> 09:00, au prorata des heures a cheval.
// Le 28/09 : soiree 6,27 kWh -> facteur 1,09.
//
// Un FACTEUR et non des kWh : le profil vient des compteurs d'energie, la
// regression des puissances integrees ; le rapport annule l'ecart de methode.
// Dans les deux sens pour le profil TYPIQUE (c'est un estimateur, une soiree
// legere allege la nuit) ; le PRUDENT ne descend jamais : max(p80, p50 recale).
// Filet : facteur borne a [0,75 ; 1,45], les extremes des 24 nuits observees
// (0,77 et 1,41 fois la moyenne) — hors de la, c'est une mesure fausse, pas une
// nuit. Facteur absent, perime (> 30 min) ou hors fenetre : profil inchange.
const NIGHT_FACTOR_MIN = 0.75;
const NIGHT_FACTOR_MAX = 1.45;
const nowMsNight = num(input.nowMs, Date.now());
const nightStartMs = num(input.night_house_start_ms, NaN);
const nightEndMs = num(input.night_house_end_ms, NaN);
const nightFactorRaw = num(input.night_house_factor, NaN);
const nightComputedMs = num(input.night_house_computed_ms, NaN);
const nightModelActive =
Number.isFinite(nightFactorRaw) && nightFactorRaw > 0 &&
Number.isFinite(nightStartMs) && Number.isFinite(nightEndMs) && nightEndMs > nightStartMs &&
nowMsNight >= nightStartMs && nowMsNight < nightEndMs &&
Number.isFinite(nightComputedMs) && nowMsNight - nightComputedMs <= 30 * 60000;
const nightFactor = nightModelActive
? Math.min(NIGHT_FACTOR_MAX, Math.max(NIGHT_FACTOR_MIN, nightFactorRaw))
: 1;
function nightShare(startMs) {
const debut = num(startMs, NaN);
if (!nightModelActive || !Number.isFinite(debut)) return 0;
const a = Math.max(debut, nightStartMs), b = Math.min(debut + 3600000, nightEndMs);
return b > a ? (b - a) / 3600000 : 0;
}
function nightScaled(profile, slot) {
const h = num(slot.hour, 0);
return (profile[h] ?? 0) * (1 + (nightFactor - 1) * nightShare(slot.startMs));
}
function buildSlots(consumption, prudent = false) {
return horizon.slice(0, CFG.SEARCH_HOURS).map(slot => {
const hour = num(slot.hour, 0);
const productionKwh = Math.max(0, num(slot.expectedKwh, 0));
const consumptionKwh = consumption[hour] ?? 0;
const consumptionKwh = prudent
? Math.max(consumption[hour] ?? 0, nightScaled(consumptionP50, slot))
: nightScaled(consumption, slot);
return {
startMs: slot.startMs,
hour,
@@ -341,7 +387,7 @@ function coverage(slots, hcFloorPct = 0) {
}
const slotsP50 = buildSlots(consumptionP50);
const slotsP80 = buildSlots(consumptionP80);
const slotsP80 = buildSlots(consumptionP80, true);
const hcFloorTypicalPct = hcReachFloorPct(slotsP50, nextHcStartMs);
const hcFloorNightPrudentPct = hcNightReachActive ? hcReachFloorPct(slotsP80, nextNightHcStartMs) : 0;
const hcFloorPrudentPct = Math.max(hcReachFloorPct(slotsP80, nextHcStartMs), hcFloorNightPrudentPct);
@@ -613,6 +659,14 @@ return [{
surplusAvailableKwh: round2(surplusAfterCrossingKwh),
},
// Nuit prevue par la soiree (28/09) : facteur applique au profil.
nightModel: {
active: nightModelActive,
factor: Math.round(nightFactor * 1000) / 1000,
rawFactor: Number.isFinite(nightFactorRaw) ? nightFactorRaw : null,
forecastKwh: num(input.night_house_forecast_kwh, NaN) || null,
},
// Voiture branchee en HC, depart avant la prochaine HC (28/09).
vehicleMidday: {
active: vehicleRuleActive,

View File

@@ -154,7 +154,11 @@ const hhmm = ms => new Date(ms).toLocaleTimeString('fr-FR', { timeZone: 'Europe/
const vehText = vm.active
? ` Voiture (départ ${hhmm(vm.departureMs)}) : ${vm.reachableKwh} kWh à donner, solaire ${vm.solarKwh} kWh → ${vm.gridNeeded ? 'réseau justifié' : 'solaire suffisant'}.`
: '';
setText('input_text.evcs_balance_reason', reason + hcText + vehText + degradedNote + ndText);
const nm = balance.nightModel || {};
const nightText = nm.active && Math.abs(nm.factor - 1) >= 0.005
? ` Nuit recalée sur la soirée : conso ×${String(nm.factor).replace('.', ',')}${nm.forecastKwh ? ` (${String(nm.forecastKwh).replace('.', ',')} kWh prévus 21:30-09:00)` : ''}.`
: '';
setText('input_text.evcs_balance_reason', reason + nightText + hcText + vehText + degradedNote + ndText);
// Trace longue durée, pour comparer jour après jour le seuil calculé au seuil
// fixe encore en vigueur — c'est cette comparaison qui décidera du câblage.

View File

@@ -59,6 +59,11 @@
{%- endif -%}
{{ {
"ts": now().isoformat(),
"night_house_factor": state_attr("sensor.evcs_nuit_maison_prevue_kwh", "facteur"),
"night_house_start_ms": state_attr("sensor.evcs_nuit_maison_prevue_kwh", "debut_ms"),
"night_house_end_ms": state_attr("sensor.evcs_nuit_maison_prevue_kwh", "fin_ms"),
"night_house_computed_ms": (as_timestamp(state_attr("sensor.evcs_nuit_maison_prevue_kwh", "calcule_a"), 0) * 1000),
"night_house_forecast_kwh": states("sensor.evcs_nuit_maison_prevue_kwh"),
"vehicle_connected": is_state("binary_sensor.victron_evcs_vehicle_connected", "on"),
"vehicle_departure_ms": (as_timestamp(vh.dep) * 1000) if vh.dep != '' else none,
"vehicle_level_pct": states("input_number.victron_evcs_vehicle_level_pct"),

View File

@@ -60,6 +60,12 @@ return [{
// Plancher de charge HC manuelle (18/09) : oublie ici jusqu'au 28/09, le
// bouton n'avait donc jamais atteint le bilan.
manual_hc_floor_percent: snapshot.manual_hc_floor_percent,
// Nuit de la maison prevue par la soiree (28/09) : facteur sur le profil.
night_house_factor: snapshot.night_house_factor,
night_house_start_ms: snapshot.night_house_start_ms,
night_house_end_ms: snapshot.night_house_end_ms,
night_house_computed_ms: snapshot.night_house_computed_ms,
night_house_forecast_kwh: snapshot.night_house_forecast_kwh,
// Vehicule branche a midi (28/09, JEEP/VW/ZERO) : depart du jour et besoin saisi.
vehicle_connected: snapshot.vehicle_connected,
vehicle_departure_ms: snapshot.vehicle_departure_ms,