Prevision contre reel : la serie que Home Assistant ne peut pas produire
Le proprietaire veut comparer prevision et production heure par heure sur une semaine, dans son tableau de bilan. Les courbes de prevision vivent dans des input_text ECRASES a chaque publication : HA n en garde aucun historique. La base, elle, a treize jours de previsions horaires et la production mesuree. Le service pousse donc la serie dans un capteur cree par l API HA, dont les attributs portent les tableaux — un input_text plafonne a 255 caracteres ne tiendrait pas 168 points. ⚠️ Premiere ecriture de ce service vers Home Assistant, qui a toujours ete lecteur. Perimetre minuscule et delibere : un capteur, son propre espace de noms, aucun actionneur. ⚠️ Et la troisieme serie n est pas decorative. Une heure BRIDEE n est pas comparable : production sous la prevision peut vouloir dire mauvaise prevision OU batterie pleine. Les mesures le montrent — le 13/09 a 15 h, prevu 5,32 et realise 1,82 avec 100 % de bridage, l ecart n est pas meteorologique ; le 15/09 a 13 h, prevu 3,92 et realise 7,12 sans aucun bridage, la prevision SOUS-estime de 82 %. Sans la bande, la courbe ferait accuser la meteo a tort. L etat du capteur porte l ecart moyen sur les seules heures comparables — non bridees — qui est le seul chiffre qui juge vraiment la prevision. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
@@ -1279,6 +1279,92 @@ async function appliquerEtiquettesEnAttente(sql, client) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// PREVISION CONTRE REEL — la serie que Home Assistant ne peut pas produire
|
||||||
|
// ---------------------------------------------------------------------
|
||||||
|
// Les courbes de prevision vivent dans des `input_text` ECRASES a chaque
|
||||||
|
// publication : Home Assistant n'en garde aucun historique exploitable. La base,
|
||||||
|
// elle, a 13 jours de previsions horaires (table `prevision`) ET la production
|
||||||
|
// mesuree. C'est donc ici qu'on peut comparer.
|
||||||
|
//
|
||||||
|
// Le service pousse la serie dans un capteur HA cree par l'API, dont les
|
||||||
|
// ATTRIBUTS portent les tableaux — un `input_text` plafonne a 255 caracteres ne
|
||||||
|
// tiendrait pas 168 points. La carte ApexCharts les lit directement.
|
||||||
|
//
|
||||||
|
// ⚠️ C'est la PREMIERE ecriture de ce service vers Home Assistant : il a
|
||||||
|
// toujours ete lecteur. Le perimetre est volontairement minuscule — UN capteur,
|
||||||
|
// dans son propre espace de noms, aucun actionneur, et un capteur cree par API
|
||||||
|
// disparait au redemarrage de HA puis se recree au cycle suivant.
|
||||||
|
//
|
||||||
|
// ⚠️ ET LA REGLE DE LECTURE EST DANS LA DONNEE : une heure BRIDEE n'est pas
|
||||||
|
// comparable. Production sous la prevision peut vouloir dire mauvaise prevision
|
||||||
|
// 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.
|
||||||
|
async function publierPrevisionContreReel(client) {
|
||||||
|
if (DRY) return 0;
|
||||||
|
const { rows } = await client.query(`
|
||||||
|
with prev as (
|
||||||
|
select distinct on (jour) jour, horaire from prevision
|
||||||
|
where jour >= current_date - 6
|
||||||
|
and (publie_a at time zone 'Europe/Paris')::time < time '10:00'
|
||||||
|
order by jour, publie_a desc),
|
||||||
|
predite as (select p.jour + (h.i - 1) * interval '1 hour' as heure, h.v kwh
|
||||||
|
from prev p, unnest(p.horaire) with ordinality as h(v, i)),
|
||||||
|
mn as (select generate_series((current_date - 6)::timestamptz,
|
||||||
|
date_trunc('hour', now()), interval '1 minute') t),
|
||||||
|
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'
|
||||||
|
and ts >= current_date - 6 group by 1),
|
||||||
|
j as (select mn.t, v.v, count(v.v) over (order by mn.t) g
|
||||||
|
from mn left join v on v.t=mn.t),
|
||||||
|
r as (select t, first_value(v) over (partition by g order by t) w from j),
|
||||||
|
reelle as (select date_trunc('hour', t at time zone 'Europe/Paris') heure,
|
||||||
|
sum(w)/60/1000 kwh from r where w is not null group by 1),
|
||||||
|
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
|
||||||
|
from bilan where ts >= current_date - 6 group by 1)
|
||||||
|
select extract(epoch from h.heure at time zone 'Europe/Paris')*1000 as ms,
|
||||||
|
round(coalesce(p.kwh,0)::numeric,2)::float8 as predite,
|
||||||
|
round(coalesce(e.kwh,0)::numeric,2)::float8 as reelle,
|
||||||
|
round(coalesce(c.taux,0)::numeric*100)::float8 as bridage
|
||||||
|
from (select heure from predite union select heure from reelle) h
|
||||||
|
left join predite p on p.heure=h.heure
|
||||||
|
left join reelle e on e.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 <= date_trunc('hour', now() at time zone 'Europe/Paris')
|
||||||
|
order by h.heure`);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
const comparables = rows.filter((x) => x.bridage < 20 && (x.predite > 0.2 || x.reelle > 0.2));
|
||||||
|
const sp = comparables.reduce((a, x) => a + x.predite, 0);
|
||||||
|
const sr = comparables.reduce((a, x) => a + x.reelle, 0);
|
||||||
|
const ecart = sp > 0 ? Math.round((sr / sp - 1) * 100) : null;
|
||||||
|
|
||||||
|
const corps = {
|
||||||
|
state: ecart === null ? 'unknown' : String(ecart),
|
||||||
|
attributes: {
|
||||||
|
friendly_name: 'Prévision solaire — écart sur les heures comparables',
|
||||||
|
unit_of_measurement: '%',
|
||||||
|
icon: 'mdi:chart-timeline-variant',
|
||||||
|
heures_comparables: comparables.length,
|
||||||
|
heures_total: rows.length,
|
||||||
|
predite: rows.map((x) => [Number(x.ms), x.predite]),
|
||||||
|
reelle: rows.map((x) => [Number(x.ms), x.reelle]),
|
||||||
|
bridage: rows.map((x) => [Number(x.ms), x.bridage]),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const r = await fetch(`${HA_URL}/api/states/sensor.evcs_prevision_contre_reel`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${HA_TOKEN}`, 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(corps),
|
||||||
|
signal: AbortSignal.timeout(10000),
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||||
|
return rows.length;
|
||||||
|
}
|
||||||
|
|
||||||
async function cycle(client) {
|
async function cycle(client) {
|
||||||
// En blanc, on compte ce qu'on ECRIRAIT. Un essai a blanc qui renvoie
|
// En blanc, on compte ce qu'on ECRIRAIT. Un essai a blanc qui renvoie
|
||||||
// toujours zero ne verifie rien — c'est le piege classique du test qui ne
|
// toujours zero ne verifie rien — c'est le piege classique du test qui ne
|
||||||
@@ -1308,6 +1394,11 @@ async function cycle(client) {
|
|||||||
// Ingestion des relevés de cellule deposes par le poste au cable serie.
|
// Ingestion des relevés de cellule deposes par le poste au cable serie.
|
||||||
// Hors essai a blanc : l'insertion est massive et idempotente, la simuler
|
// Hors essai a blanc : l'insertion est massive et idempotente, la simuler
|
||||||
// n'apprendrait rien.
|
// n'apprendrait rien.
|
||||||
|
// Serie horaire : inutile de la republier toutes les minutes.
|
||||||
|
prevreel: (DRY || new Date().getMinutes() % 10 !== 3) ? 0
|
||||||
|
: await publierPrevisionContreReel(client).catch((e) => {
|
||||||
|
console.error(`prevision contre reel : ${e.message}`); return 0;
|
||||||
|
}),
|
||||||
rapport: DRY ? 0 : await envoyerRapportDuMatin(sql, client).catch((e) => {
|
rapport: DRY ? 0 : await envoyerRapportDuMatin(sql, client).catch((e) => {
|
||||||
console.error('rapport de nuit :', e.message); return 0;
|
console.error('rapport de nuit :', e.message); return 0;
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user