Bilan : prevision solaire VRM (selecteur de source), les deux sources enregistrees

Decision du proprietaire (28/09) : Forecast.Solar renvoie des totaux quasi
constants (~26 kWh) pour 40-47 reels ; VRM suivait mieux le reel sur 10 jours
(erreur 21 % contre 32 %, 14 % contre 36 % sur les 5 derniers). Le bilan
lit VRM des maintenant ; les deux previsions sont enregistrees (colonne
source, cle etendue) pour les comparer dans une semaine.
- input_select.evcs_balance_forecast_source (vrm | forecast_solar) = retour
  arriere en un geste ; helpers evcs_balance_vrm_* au format du bilan
- historisation : lecture VRM (jeton dans .env, jamais dans HA) toutes les
  15 min, vrmVersCourbes (jour et heure de Paris), tests 69/69
- modele du bilan conditionnel ; bilan 50/50, deploye
- evcs_prevision_contre_reel porte sur la source active

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 01:39:54 +02:00
parent 0686ef7e7d
commit 5a8e4afde0
6 changed files with 162 additions and 13 deletions

View File

@@ -541,7 +541,8 @@ export function projectionSoir(st) {
statut: 'indetermine',
soc_actuel_pct: soc,
reste_solaire_prevu_kwh: reste,
reste_solaire_source: 'Forecast.Solar, non corrige',
reste_solaire_source: 'Forecast.Solar (sensor.energy_production_today_remaining), non corrige',
source_prevision_du_bilan: st.get('input_select.evcs_balance_forecast_source')?.state ?? null,
ecart_prevision_7j_pct: ecart,
explication: 'Je ne peux pas estimer la batterie de ce soir : la prevision solaire ' +
`sous-estime la production${ecart !== null ? ` d'environ ${Math.round(ecart)} % ces sept derniers jours` : ''}, ` +
@@ -595,7 +596,10 @@ export function previsionNuit(st, maintenant = new Date()) {
contacteur_edf: reseau, session_reseau_nuit_engagee: session,
voiture: branche === 'on' ? 'branchee' : branche === 'off' ? 'aucune' : 'inconnue',
bilan_calcule_il_y_a_min: age,
reserve_prevision: 'la production du matin vient de Forecast.Solar, qui sous-estime ces jours-ci : l heure de retour du solaire peut etre plus precoce',
source_prevision: st.get('input_select.evcs_balance_forecast_source')?.state ?? null,
reserve_prevision: st.get('input_select.evcs_balance_forecast_source')?.state === 'vrm'
? 'la production du matin vient de la prevision VRM, en essai depuis le 28/09 : l heure de retour du solaire reste une estimation'
: 'la production du matin vient de Forecast.Solar, qui sous-estime ces jours-ci : l heure de retour du solaire peut etre plus precoce',
};
if (statut !== 'ok' || age === null || age > BILAN_FRAIS_MIN || aCouvrir === null || retour === null) {
return { etat: 'indisponible', attributs: { ...attributs, statut: 'indisponible',
@@ -614,3 +618,40 @@ export function previsionNuit(st, maintenant = new Date()) {
`${session ? ' La session reseau de la nuit est engagee.' : ''}`;
return { etat: reseauNecessaire ? reseauKwh : 0, attributs: { ...attributs, statut: 'frais', explication: texte, texte_rapport: texte } };
}
// ---------------------------------------------------------------------
// Prevision VRM -> courbes horaires au format du bilan (28/09/2026)
// ---------------------------------------------------------------------
// VRM (API v2, stats?type=forecast&interval=hours) renvoie des paires
// [ms UTC debut d'heure, Wh]. Le bilan attend, par jour DE PARIS, 24 valeurs en
// kWh (index 0 = 00 h locale), une date AAAA-MM-JJ et un horodatage
// « JJ/MM HH:MM » : exactement ce que produit la chaine Forecast.Solar, pour
// que le bilan lise les deux sources indifferemment.
const jourParis = (ms) => new Date(ms).toLocaleDateString('sv-SE', { timeZone: 'Europe/Paris' });
const heureNumParis = (ms) => Number(new Date(ms).toLocaleString('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', hour12: false }));
export function vrmVersCourbes(paires, maintenant = new Date()) {
const jours = new Map();
for (const [ms, wh] of paires || []) {
const v = Number(wh);
if (!Number.isFinite(Number(ms)) || !Number.isFinite(v)) continue;
const j = jourParis(Number(ms));
if (!jours.has(j)) jours.set(j, new Array(24).fill(0));
// Heure d'hiver/ete : l'heure en double du 25 octobre s'ajoute, jamais ne s'ecrase.
jours.get(j)[heureNumParis(Number(ms)) % 24] += Math.max(0, v) / 1000;
}
const auj = jourParis(maintenant.getTime());
const dem = jourParis(maintenant.getTime() + 86400000);
const csv = (a) => a.map((x) => (Math.round(x * 100) / 100).toFixed(2)).join(',');
const total = (a) => Math.round(a.reduce((s, x) => s + x, 0) * 10) / 10;
const tA = jours.get(auj), tD = jours.get(dem);
const d = new Date(maintenant.toLocaleString('en-US', { timeZone: 'Europe/Paris' }));
const p2 = (n) => String(n).padStart(2, '0');
return {
ok: !!tA,
day0: auj,
today: tA ? { csv: csv(tA), totalKwh: total(tA), horaire: tA.map((x) => Math.round(x * 100) / 100) } : null,
tomorrow: tD ? { csv: csv(tD), totalKwh: total(tD), horaire: tD.map((x) => Math.round(x * 100) / 100) } : null,
updated: `${p2(d.getDate())}/${p2(d.getMonth() + 1)} ${p2(d.getHours())}:${p2(d.getMinutes())}`,
};
}