La prevision affichee cesse d etre de l histoire
Le graphe « production prevue et realisee heure par heure » du troisieme onglet du bilan energetique ne montrait que du passe : sa courbe « prevue » etait la prevision publiee le matin de chaque jour ECOULE, et la fenetre s arretait a l heure en cours. C est un instrument de mesure du biais, pas une prevision. Deux courbes desormais, et elles n ont ni la meme provenance ni le meme usage. Pour un jour ecoule, ce qui avait ete annonce LE MATIN MEME reste la seule reference honnete : une prevision revisee a 18 h connait deja la journee, la comparer au reel ne mesure plus rien. Pour les heures a venir, c est la DERNIERE publication qui fait foi — aujourd hui jusqu au soir, demain en entier. Les deux se touchent sur l heure en cours au lieu de laisser un trou, et la seconde est tracee en pointilles. Au-dela de l heure en cours, « reel » et « bridage » valent NULL et non zero : un zero dessinerait une chute de production qui n a pas eu lieu. Defaut trouve en chemin : l ecart moyen comptait l heure EN COURS, mesuree sur ses seules minutes ecoulees. A 14h05 son reel vaut cinq minutes de production contre une prevision d heure pleine, soit -92 % qui n existent pas. La colonne `clos` l exclut. Et la serie mesuree s arrete desormais a la minute courante et non au debut de l heure, si bien que l heure en cours est reellement tracee au lieu de valoir zero. Le partage aujourd hui / demain se fait en SQL, dans le fuseau deja utilise par le reste de la requete : le conteneur repond en UTC a `date`, il ne doit pas servir d arbitre. Fenetre de la carte portee a hier / aujourd hui / demain (graph_span 3d, span end day offset +1d) : sans cela la prevision, qui est justement ce qu on veut voir, tombe hors du cadre. Verifie dans le paquet installe : `span.offset` exige bien un signe, et l offset decale les deux bornes. Verification en production : 47 points de prevision publies, du 17/09 01 h au 18/09 23 h, 33,3 kWh encore attendus aujourd hui et 34,5 annonces pour demain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
@@ -320,7 +320,15 @@ views:
|
|||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
content: >
|
content: >
|
||||||
## Comment lire cette courbe
|
## Comment lire ces courbes
|
||||||
|
|
||||||
|
|
||||||
|
**La courbe pleine orange est de l'histoire, la pointillée est la
|
||||||
|
prévision.** Pour un jour écoulé on affiche ce qui avait été annoncé
|
||||||
|
*le matin même* — c'est la seule référence qui mesure vraiment la
|
||||||
|
qualité de la prévision, une version révisée à 18 h connaissant déjà
|
||||||
|
la journée. Pour les heures à venir on affiche la **dernière**
|
||||||
|
publication : aujourd'hui jusqu'à ce soir, et demain en entier.
|
||||||
|
|
||||||
|
|
||||||
⚠️ **Une heure bridée n'est pas comparable.** Quand la batterie est
|
⚠️ **Une heure bridée n'est pas comparable.** Quand la batterie est
|
||||||
@@ -329,8 +337,9 @@ views:
|
|||||||
soit fausse. La bande grise du bas dit quand c'est le cas.
|
soit fausse. La bande grise du bas dit quand c'est le cas.
|
||||||
|
|
||||||
|
|
||||||
Le chiffre en tête — l'écart moyen — ne compte **que les heures non
|
Le chiffre en tête — l'écart moyen sur sept jours — ne compte **que
|
||||||
bridées**. C'est le seul qui juge vraiment la prévision.
|
les heures non bridées et déjà terminées**. C'est le seul qui juge
|
||||||
|
vraiment la prévision.
|
||||||
|
|
||||||
|
|
||||||
Deux exemples mesurés : le 13/09 à 15 h, prévu 5,32 kWh et réalisé
|
Deux exemples mesurés : le 13/09 à 15 h, prévu 5,32 kWh et réalisé
|
||||||
@@ -340,8 +349,21 @@ views:
|
|||||||
|
|
||||||
- type: entities
|
- type: entities
|
||||||
entities:
|
entities:
|
||||||
|
- type: attribute
|
||||||
|
entity: sensor.evcs_prevision_contre_reel
|
||||||
|
attribute: reste_aujourdhui_kwh
|
||||||
|
name: Encore attendu aujourd'hui
|
||||||
|
suffix: " kWh"
|
||||||
|
icon: mdi:weather-sunny
|
||||||
|
- type: attribute
|
||||||
|
entity: sensor.evcs_prevision_contre_reel
|
||||||
|
attribute: annonce_demain_kwh
|
||||||
|
name: Annoncé pour demain
|
||||||
|
suffix: " kWh"
|
||||||
|
icon: mdi:weather-sunset-up
|
||||||
|
- type: divider
|
||||||
- entity: sensor.evcs_prevision_contre_reel
|
- entity: sensor.evcs_prevision_contre_reel
|
||||||
name: Écart moyen (heures comparables)
|
name: Écart moyen (heures comparables, 7 j)
|
||||||
- type: attribute
|
- type: attribute
|
||||||
entity: sensor.evcs_prevision_contre_reel
|
entity: sensor.evcs_prevision_contre_reel
|
||||||
attribute: heures_comparables
|
attribute: heures_comparables
|
||||||
@@ -349,49 +371,65 @@ views:
|
|||||||
- type: attribute
|
- type: attribute
|
||||||
entity: sensor.evcs_prevision_contre_reel
|
entity: sensor.evcs_prevision_contre_reel
|
||||||
attribute: heures_total
|
attribute: heures_total
|
||||||
name: Heures dans la fenêtre
|
name: Heures terminées dans la fenêtre
|
||||||
|
|
||||||
- type: custom:apexcharts-card
|
- type: custom:apexcharts-card
|
||||||
header:
|
header:
|
||||||
show: true
|
show: true
|
||||||
title: Production prévue et réalisée, heure par heure
|
title: Production réalisée, et ce qui est annoncé jusqu'à demain soir
|
||||||
show_states: false
|
show_states: false
|
||||||
graph_span: 7d
|
# Hier, aujourd'hui, demain. La fenêtre s'arrête a la fin de DEMAIN et
|
||||||
|
# non a l'heure en cours : sans cela la prevision, qui est justement ce
|
||||||
|
# qu'on veut voir, tombe hors du cadre.
|
||||||
|
graph_span: 3d
|
||||||
span:
|
span:
|
||||||
end: hour
|
end: day
|
||||||
|
offset: '+1d'
|
||||||
apex_config:
|
apex_config:
|
||||||
chart:
|
chart:
|
||||||
height: 340
|
height: 340
|
||||||
stroke:
|
stroke:
|
||||||
width: [2, 2]
|
width: [1, 2, 2]
|
||||||
|
dashArray: [0, 0, 5]
|
||||||
legend:
|
legend:
|
||||||
position: top
|
position: top
|
||||||
yaxis:
|
yaxis:
|
||||||
- title: {text: kWh par heure}
|
- title: {text: kWh par heure}
|
||||||
decimals: 1
|
decimals: 1
|
||||||
|
min: 0
|
||||||
series:
|
series:
|
||||||
- entity: sensor.evcs_prevision_contre_reel
|
|
||||||
name: Prévue
|
|
||||||
type: line
|
|
||||||
color: '#f0a202'
|
|
||||||
data_generator: |
|
|
||||||
return (entity.attributes.predite || []);
|
|
||||||
- entity: sensor.evcs_prevision_contre_reel
|
- entity: sensor.evcs_prevision_contre_reel
|
||||||
name: Réalisée
|
name: Réalisée
|
||||||
type: area
|
type: area
|
||||||
opacity: 0.25
|
opacity: 0.25
|
||||||
color: '#2d8cf0'
|
color: '#2d8cf0'
|
||||||
data_generator: |
|
data_generator: |
|
||||||
return (entity.attributes.reelle || []);
|
return (entity.attributes.reelle || [])
|
||||||
|
.filter(p => p[1] !== null && p[1] !== undefined);
|
||||||
|
- entity: sensor.evcs_prevision_contre_reel
|
||||||
|
name: Prévue le matin même
|
||||||
|
type: line
|
||||||
|
color: '#f0a202'
|
||||||
|
data_generator: |
|
||||||
|
return (entity.attributes.predite || [])
|
||||||
|
.filter(p => p[1] !== null && p[1] !== undefined);
|
||||||
|
- entity: sensor.evcs_prevision_contre_reel
|
||||||
|
name: Prévision en cours
|
||||||
|
type: line
|
||||||
|
color: '#f05a02'
|
||||||
|
data_generator: |
|
||||||
|
return (entity.attributes.attendue || [])
|
||||||
|
.filter(p => p[1] !== null && p[1] !== undefined);
|
||||||
|
|
||||||
- type: custom:apexcharts-card
|
- type: custom:apexcharts-card
|
||||||
header:
|
header:
|
||||||
show: true
|
show: true
|
||||||
title: Part de l'heure où la production était bridée
|
title: Part de l'heure où la production était bridée
|
||||||
show_states: false
|
show_states: false
|
||||||
graph_span: 7d
|
graph_span: 3d
|
||||||
span:
|
span:
|
||||||
end: hour
|
end: day
|
||||||
|
offset: '+1d'
|
||||||
apex_config:
|
apex_config:
|
||||||
chart:
|
chart:
|
||||||
height: 150
|
height: 150
|
||||||
@@ -408,4 +446,5 @@ views:
|
|||||||
opacity: 0.4
|
opacity: 0.4
|
||||||
color: '#9e9e9e'
|
color: '#9e9e9e'
|
||||||
data_generator: |
|
data_generator: |
|
||||||
return (entity.attributes.bridage || []);
|
return (entity.attributes.bridage || [])
|
||||||
|
.filter(p => p[1] !== null && p[1] !== undefined);
|
||||||
|
|||||||
@@ -1375,18 +1375,39 @@ async function appliquerEtiquettesEnAttente(sql, client) {
|
|||||||
// comparable. Production sous la prevision peut vouloir dire mauvaise prevision
|
// comparable. Production sous la prevision peut vouloir dire mauvaise prevision
|
||||||
// OU batterie pleine, et seules les heures non bridees permettent de juger.
|
// OU batterie pleine, et seules les heures non bridees permettent de juger.
|
||||||
// D'ou la troisieme serie, sans laquelle la carte ferait accuser la meteo a tort.
|
// D'ou la troisieme serie, sans laquelle la carte ferait accuser la meteo a tort.
|
||||||
|
//
|
||||||
|
// ⚠️ DEUX COURBES DE PREVISION, ET ELLES NE DISENT PAS LA MEME CHOSE. Pour un
|
||||||
|
// jour ECOULE, la seule reference honnete est ce qui avait ete annonce LE MATIN
|
||||||
|
// MEME : une prevision revisee a 18 h connait deja la journee, la comparer au
|
||||||
|
// reel ne mesure plus rien. Mais cette courbe-la est de l'HISTOIRE, et elle ne
|
||||||
|
// dit rien de ce qui vient. La prevision au sens propre — aujourd'hui jusqu'a
|
||||||
|
// ce soir, demain en entier — est la DERNIERE publiee, et c'est la serie
|
||||||
|
// `attendue`. Ne pas les fondre en une seule : elles n'ont ni la meme
|
||||||
|
// provenance ni le meme usage.
|
||||||
async function publierPrevisionContreReel(client) {
|
async function publierPrevisionContreReel(client) {
|
||||||
if (DRY) return 0;
|
if (DRY) return 0;
|
||||||
const { rows } = await client.query(`
|
const { rows } = await client.query(`
|
||||||
with prev as (
|
-- Ce qui etait ANNONCE LE MATIN pour un jour deja ecoule. C'est la seule
|
||||||
|
-- reference qui mesure la qualite d'une prevision : celle qui n'a pas ete
|
||||||
|
-- revisee depuis. Mais c'est de l'HISTOIRE, pas une prevision.
|
||||||
|
with matin as (
|
||||||
select distinct on (jour) jour, horaire from prevision
|
select distinct on (jour) jour, horaire from prevision
|
||||||
where jour >= current_date - 6
|
where jour between current_date - 6 and current_date
|
||||||
and (publie_a at time zone 'Europe/Paris')::time < time '10:00'
|
and (publie_a at time zone 'Europe/Paris')::time < time '10:00'
|
||||||
order by jour, publie_a desc),
|
order by jour, publie_a desc),
|
||||||
predite as (select p.jour + (h.i - 1) * interval '1 hour' as heure, h.v kwh
|
-- Ce qui est annonce MAINTENANT pour les heures a venir : la prevision au
|
||||||
from prev p, unnest(p.horaire) with ordinality as h(v, i)),
|
-- sens propre, aujourd'hui jusqu'a ce soir et demain en entier.
|
||||||
|
courante as (
|
||||||
|
select distinct on (jour) jour, horaire from prevision
|
||||||
|
where jour >= current_date
|
||||||
|
order by jour, publie_a desc),
|
||||||
|
maintenant as (select date_trunc('hour', now() at time zone 'Europe/Paris') h),
|
||||||
|
passee as (select m.jour + (u.i - 1) * interval '1 hour' as heure, u.v kwh
|
||||||
|
from matin m, unnest(m.horaire) with ordinality as u(v, i)),
|
||||||
|
futur as (select c.jour + (u.i - 1) * interval '1 hour' as heure, u.v kwh
|
||||||
|
from courante c, unnest(c.horaire) with ordinality as u(v, i)),
|
||||||
mn as (select generate_series((current_date - 6)::timestamptz,
|
mn as (select generate_series((current_date - 6)::timestamptz,
|
||||||
date_trunc('hour', now()), interval '1 minute') t),
|
date_trunc('minute', now()), interval '1 minute') t),
|
||||||
v as (select date_trunc('minute', ts) t, (array_agg(valeur order by ts desc))[1] v
|
v as (select date_trunc('minute', ts) t, (array_agg(valeur order by ts desc))[1] v
|
||||||
from mesure where cle='sensor.victron_pv_power_system'
|
from mesure where cle='sensor.victron_pv_power_system'
|
||||||
and ts >= current_date - 6 group by 1),
|
and ts >= current_date - 6 group by 1),
|
||||||
@@ -1398,25 +1419,45 @@ async function publierPrevisionContreReel(client) {
|
|||||||
censure as (select date_trunc('hour', ts at time zone 'Europe/Paris') heure,
|
censure as (select date_trunc('hour', ts at time zone 'Europe/Paris') heure,
|
||||||
avg(case when pv_censure then 1.0 else 0.0 end) taux
|
avg(case when pv_censure then 1.0 else 0.0 end) taux
|
||||||
from bilan where ts >= current_date - 6 group by 1)
|
from bilan where ts >= current_date - 6 group by 1)
|
||||||
|
-- Les series passees s'arretent a l'heure en cours, les futures y
|
||||||
|
-- commencent : les deux courbes se TOUCHENT sur cette heure-la au lieu de
|
||||||
|
-- laisser un trou. Au-dela, "reel" et "bridage" valent NULL et non zero —
|
||||||
|
-- un zero dessinerait une chute de production qui n'a pas eu lieu.
|
||||||
select extract(epoch from h.heure at time zone 'Europe/Paris')*1000 as ms,
|
select extract(epoch from h.heure at time zone 'Europe/Paris')*1000 as ms,
|
||||||
round(coalesce(p.kwh,0)::numeric,2)::float8 as predite,
|
case when h.heure <= n.h then round(coalesce(p.kwh,0)::numeric,2)::float8 end as predite,
|
||||||
round(coalesce(e.kwh,0)::numeric,2)::float8 as reelle,
|
case when h.heure >= n.h then round(f.kwh::numeric,2)::float8 end as attendue,
|
||||||
round(coalesce(c.taux,0)::numeric*100)::float8 as bridage
|
case when h.heure <= n.h then round(coalesce(e.kwh,0)::numeric,2)::float8 end as reelle,
|
||||||
from (select heure from predite union select heure from reelle) h
|
case when h.heure <= n.h then round(coalesce(c.taux,0)::numeric*100)::float8 end as bridage,
|
||||||
left join predite p on p.heure=h.heure
|
(h.heure < n.h) as clos,
|
||||||
|
(h.heure::date = (now() at time zone 'Europe/Paris')::date) as aujourdhui
|
||||||
|
from (select heure from passee union select heure from futur
|
||||||
|
union select heure from reelle) h
|
||||||
|
cross join maintenant n
|
||||||
|
left join passee p on p.heure=h.heure
|
||||||
|
left join futur f on f.heure=h.heure
|
||||||
left join reelle e on e.heure=h.heure
|
left join reelle e on e.heure=h.heure
|
||||||
left join censure c on c.heure=h.heure
|
left join censure c on c.heure=h.heure
|
||||||
-- Pas d'heures futures : leur "reel" vaut 0 et creuserait un faux ecart.
|
where h.heure >= current_date - 6
|
||||||
where h.heure <= date_trunc('hour', now() at time zone 'Europe/Paris')
|
|
||||||
order by h.heure`);
|
order by h.heure`);
|
||||||
|
|
||||||
// Ecart moyen sur les seules heures COMPARABLES : non bridees, et ou quelque
|
// Ecart moyen sur les seules heures COMPARABLES : non bridees, et ou quelque
|
||||||
// chose etait attendu ou produit. C'est le seul chiffre qui juge la prevision.
|
// chose etait attendu ou produit. C'est le seul chiffre qui juge la prevision.
|
||||||
const comparables = rows.filter((x) => x.bridage < 20 && (x.predite > 0.2 || x.reelle > 0.2));
|
//
|
||||||
|
// ⚠️ `clos` EXCLUT L'HEURE EN COURS. Elle n'est mesuree que pour les minutes
|
||||||
|
// deja ecoulees : a 14h05 son "reel" vaut cinq minutes de production contre
|
||||||
|
// une prevision d'heure pleine, soit un ecart de -92 % qui n'existe pas.
|
||||||
|
const comparables = rows.filter((x) => x.clos && x.bridage < 20
|
||||||
|
&& (x.predite > 0.2 || x.reelle > 0.2));
|
||||||
const sp = comparables.reduce((a, x) => a + x.predite, 0);
|
const sp = comparables.reduce((a, x) => a + x.predite, 0);
|
||||||
const sr = comparables.reduce((a, x) => a + x.reelle, 0);
|
const sr = comparables.reduce((a, x) => a + x.reelle, 0);
|
||||||
const ecart = sp > 0 ? Math.round((sr / sp - 1) * 100) : null;
|
const ecart = sp > 0 ? Math.round((sr / sp - 1) * 100) : null;
|
||||||
|
|
||||||
|
// Le partage aujourd'hui / demain se fait EN SQL, dans le fuseau deja utilise
|
||||||
|
// par tout le reste de la requete. Le refaire en JavaScript reviendrait a
|
||||||
|
// parier sur le fuseau du conteneur — qui est en UTC pour `date`, et ne doit
|
||||||
|
// donc pas servir d'arbitre.
|
||||||
|
const aVenir = rows.filter((x) => x.attendue !== null && x.attendue !== undefined);
|
||||||
|
const somme = (f) => Math.round(aVenir.filter(f).reduce((a, x) => a + x.attendue, 0) * 10) / 10;
|
||||||
const corps = {
|
const corps = {
|
||||||
state: ecart === null ? 'unknown' : String(ecart),
|
state: ecart === null ? 'unknown' : String(ecart),
|
||||||
attributes: {
|
attributes: {
|
||||||
@@ -1424,8 +1465,11 @@ async function publierPrevisionContreReel(client) {
|
|||||||
unit_of_measurement: '%',
|
unit_of_measurement: '%',
|
||||||
icon: 'mdi:chart-timeline-variant',
|
icon: 'mdi:chart-timeline-variant',
|
||||||
heures_comparables: comparables.length,
|
heures_comparables: comparables.length,
|
||||||
heures_total: rows.length,
|
heures_total: rows.filter((x) => x.clos).length,
|
||||||
|
reste_aujourdhui_kwh: somme((x) => x.aujourdhui),
|
||||||
|
annonce_demain_kwh: somme((x) => !x.aujourdhui),
|
||||||
predite: rows.map((x) => [Number(x.ms), x.predite]),
|
predite: rows.map((x) => [Number(x.ms), x.predite]),
|
||||||
|
attendue: rows.map((x) => [Number(x.ms), x.attendue]),
|
||||||
reelle: rows.map((x) => [Number(x.ms), x.reelle]),
|
reelle: rows.map((x) => [Number(x.ms), x.reelle]),
|
||||||
bridage: rows.map((x) => [Number(x.ms), x.bridage]),
|
bridage: rows.map((x) => [Number(x.ms), x.bridage]),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user