Les requetes s'executaient, les grandeurs etaient fausses

Trois defauts trouves en LISANT les tableaux, qu'aucune des 19 requetes ne
signalait puisqu'elles s'executaient toutes sans erreur.

1. `historise-evcs.mjs` horodate chaque mesure avec son propre last_updated —
   le vrai instant de la mesure, et c'est le bon choix. Mais deux grandeurs ne
   partagent alors presque jamais le meme ts : 2656 lignes pour 2656 instants
   distincts. Mes GROUP BY ts rendaient une ligne par grandeur avec une seule
   colonne remplie, donc la courbe "Excedent" — la grandeur meme du §36 —
   etait VIDE sur les 2650 lignes.

2. Le service n'ecrit que les changements, donc un regroupement a la minute
   laisse des trous : la difference n'existait que 73 minutes sur 434. Report
   de la derniere valeur connue, ce que fait aussi le controleur en temps
   reel. 430/435 apres.

3. L'energie horaire par sum(valeur)/60 suppose 60 echantillons par heure ; il
   y en a eu 26 a 47. Integration ponderee par la duree, plafonnee a 300 s
   pour qu'un arret du service n'extrapole pas une valeur perimee. ~9 %
   d'ecart. Le "25 a 58 %" que j'avais d'abord annonce etait faux : il
   comparait la somme a la moyenne, qui ignore la duree.

Plus deux corrections d'affichage : la colonne `jour` s'affichait
"2026-09-04 02:00:00" (date nue rendue comme instant UTC), et le decompte des
chargeurs MPPT est de six entites publiees, pas sept.

Lecon : verifier-dashboards.mjs prouve qu'une requete s'EXECUTE, pas qu'elle
rend la grandeur voulue — une colonne integralement nulle passe sans un mot.
Ce sont les courbes regardees a l'ecran qui ont attrape les trois.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-09-04 08:48:42 +02:00
parent d001cbbf2a
commit 64aee4bfcc
5 changed files with 121 additions and 43 deletions

View File

@@ -3527,3 +3527,43 @@ Les evenements apparaissent en marqueurs verticaux sur **tous** les graphiques.
--- ---
*Document mis a jour le : 04/09/2026 (~08:30) — Grafana provisionne par git, 19/19 requetes verifiees* *Document mis a jour le : 04/09/2026 (~08:30) — Grafana provisionne par git, 19/19 requetes verifiees*
### Correction du meme jour — les requetes marchaient, les grandeurs non
Trois defauts trouves en LISANT les tableaux, qu'aucune des 19 requetes ne
signalait puisqu'elles s'executaient toutes sans erreur :
1. **`ts` n'est pas un instant de cycle.** `historise-evcs.mjs` horodate chaque
mesure avec son propre `last_updated` — le vrai instant de la mesure, et
c'est le bon choix. Mais deux grandeurs ne partagent alors presque jamais le
meme `ts` : **2656 lignes pour 2656 instants distincts**. Un `GROUP BY ts`
rend donc une ligne par grandeur avec une seule colonne remplie, et toute
colonne CALCULEE est nulle. **La courbe « Excedent » — la grandeur meme du
§36 — etait vide sur les 2650 lignes**, sans que rien ne le signale.
2. **Le service n'ecrit que les changements** (`ON CONFLICT DO NOTHING` sur
`(cle, ts)`). Un simple regroupement a la minute laisse donc des trous : la
difference n'existait que les 73 minutes sur 434 ou les deux grandeurs
avaient bouge ensemble. Correction : report de la derniere valeur connue
(`first_value` sur les paliers numerotes par `count(x) OVER (ORDER BY t)`),
qui est aussi ce que fait le controleur en temps reel. **430/435 apres.**
3. **L'energie horaire par `sum(valeur)/60`** suppose 60 echantillons par
heure ; il y en a eu 26 a 47 le 04/09. Remplace par une integration
ponderee par la duree (`valeur × intervalle jusqu'au point suivant`,
plafonne a 300 s pour qu'un arret du service n'extrapole pas une valeur
perimee). Ecart mesure : ~9 %.
⚠️ Le chiffre de « 25 a 58 % » que j'avais d'abord annonce etait faux : il
comparait la somme a la MOYENNE, qui ignore la duree et n'est donc pas la
reference.
Plus deux corrections d'affichage : la colonne `jour` (type `date`) s'affichait
`2026-09-04 02:00:00`, rendue comme un instant UTC converti en heure de Paris
(`to_char` desormais) ; et le decompte des chargeurs MPPT est de **six
entites** publiees, pas sept comme l'affirmait le §28.
**Lecon**, dans la lignee des trois oscillateurs : `verifier-dashboards.mjs`
prouve qu'une requete s'EXECUTE, pas qu'elle rend la grandeur voulue. Une
colonne integralement nulle passe le controle sans un mot. Le seul test qui a
attrape ces trois defauts est d'avoir regarde les courbes et compare a ce qu'on
savait de la nuit.
---
*Document mis a jour le : 04/09/2026 (~09:00) — trois defauts de grandeur corriges dans les tableaux, 19/19*

View File

@@ -356,7 +356,7 @@
"format": "time_series", "format": "time_series",
"rawQuery": true, "rawQuery": true,
"editorMode": "code", "editorMode": "code",
"rawSql": "SELECT ts AS time,\n max(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS \"Reseau\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_power') AS \"Batterie\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Maison\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS \"Voiture\"\nFROM mesure\nWHERE $__timeFilter(ts)\nGROUP BY ts\nORDER BY ts" "rawSql": "WITH par_minute AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS Reseau,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_battery_power') AS Batterie,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS Maison,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS Voiture\n FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npaliers AS (\n SELECT t, Reseau, Batterie, Maison, Voiture,\n count(Reseau) OVER (ORDER BY t) AS g_Reseau,\n count(Batterie) OVER (ORDER BY t) AS g_Batterie,\n count(Maison) OVER (ORDER BY t) AS g_Maison,\n count(Voiture) OVER (ORDER BY t) AS g_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n first_value(Reseau) OVER (PARTITION BY g_Reseau ORDER BY t) AS Reseau,\n first_value(Batterie) OVER (PARTITION BY g_Batterie ORDER BY t) AS Batterie,\n first_value(Maison) OVER (PARTITION BY g_Maison ORDER BY t) AS Maison,\n first_value(Voiture) OVER (PARTITION BY g_Voiture ORDER BY t) AS Voiture\n FROM paliers\n)\nSELECT t AS time,\n Reseau AS \"Reseau\",\n Batterie AS \"Batterie\",\n Maison AS \"Maison\",\n Voiture AS \"Voiture\"\nFROM report ORDER BY t"
} }
], ],
"gridPos": { "gridPos": {

View File

@@ -74,7 +74,7 @@
"format": "table", "format": "table",
"rawQuery": true, "rawQuery": true,
"editorMode": "code", "editorMode": "code",
"rawSql": "WITH du_matin AS (\n SELECT DISTINCT ON (jour) jour, publie_a, horaire\n FROM prevision\n WHERE publie_a::date = jour\n AND jour BETWEEN $__timeFrom()::date AND $__timeTo()::date\n ORDER BY jour, publie_a ASC\n),\nprevu AS (\n SELECT (jour + (h - 1) * interval '1 hour') AS heure,\n horaire[h] AS kwh\n FROM du_matin, generate_subscripts(horaire, 1) AS h\n),\nproduit AS (\n -- Un echantillon toutes les 60 s : chaque point vaut 1/60 h.\n SELECT date_trunc('hour', ts) AS heure,\n sum(valeur) / 60.0 / 1000.0 AS kwh\n FROM mesure\n WHERE cle = 'sensor.victron_pv_power_system' AND $__timeFilter(ts)\n GROUP BY 1\n)\nSELECT coalesce(prevu.heure, produit.heure) AS time,\n round(prevu.kwh::numeric, 2) AS \"Prevu le matin meme\",\n round(produit.kwh::numeric, 2) AS \"Reellement produit\"\nFROM prevu FULL OUTER JOIN produit USING (heure)\nWHERE coalesce(prevu.heure, produit.heure) BETWEEN $__timeFrom() AND $__timeTo()\nORDER BY 1" "rawSql": "WITH du_matin AS (\n SELECT DISTINCT ON (jour) jour, publie_a, horaire\n FROM prevision\n WHERE publie_a::date = jour\n AND jour BETWEEN $__timeFrom()::date AND $__timeTo()::date\n ORDER BY jour, publie_a ASC\n),\nprevu AS (\n SELECT (jour + (h - 1) * interval '1 hour') AS heure,\n horaire[h] AS kwh\n FROM du_matin, generate_subscripts(horaire, 1) AS h\n),\n-- INTEGRATION PONDEREE PAR LA DUREE, et non somme/60.\n-- Le service n'ecrit une mesure que lorsqu'elle change : compter les\n-- echantillons revient a supposer qu'il y en a 60 par heure alors qu'il y\n-- en a eu 26 a 47 le 04/09 : l'energie etait sous-estimee d'environ 9 %.\n-- Chaque point vaut donc son intervalle jusqu'au suivant, plafonne a 300 s\n-- pour qu'un arret du service n'extrapole pas une valeur perimee.\npas AS (\n SELECT ts, valeur,\n LEAST(EXTRACT(epoch FROM (lead(ts) OVER (ORDER BY ts) - ts)), 300) AS dt\n FROM mesure\n WHERE cle = 'sensor.victron_pv_power_system' AND $__timeFilter(ts)\n),\nproduit AS (\n SELECT date_trunc('hour', ts) AS heure,\n sum(valeur * coalesce(dt, 0)) / 3600.0 / 1000.0 AS kwh\n FROM pas GROUP BY 1\n)\nSELECT to_char(coalesce(prevu.heure, produit.heure), 'DD/MM HH24') || ' h' AS \"Heure\",\n round(prevu.kwh::numeric, 2) AS \"Prevu le matin meme\",\n round(produit.kwh::numeric, 2) AS \"Reellement produit\"\nFROM prevu FULL OUTER JOIN produit USING (heure)\nWHERE coalesce(prevu.heure, produit.heure) BETWEEN $__timeFrom() AND $__timeTo()\nORDER BY coalesce(prevu.heure, produit.heure)"
} }
], ],
"gridPos": { "gridPos": {
@@ -205,7 +205,7 @@
"format": "table", "format": "table",
"rawQuery": true, "rawQuery": true,
"editorMode": "code", "editorMode": "code",
"rawSql": "SELECT publie_a AS \"Publiee le\", jour AS \"Pour le jour\",\n round(total_kwh::numeric, 1) AS \"Total annonce (kWh)\",\n round((horaire[8] + horaire[9] + horaire[10] + horaire[11])::numeric, 2) AS \"07-11 h\",\n round((horaire[12] + horaire[13] + horaire[14] + horaire[15])::numeric, 2) AS \"11-15 h\",\n round((horaire[16] + horaire[17] + horaire[18] + horaire[19])::numeric, 2) AS \"15-19 h\"\nFROM prevision\nWHERE $__timeFilter(publie_a)\nORDER BY publie_a DESC, jour" "rawSql": "SELECT publie_a AS \"Publiee le\",\n to_char(jour, 'DD/MM') AS \"Pour le jour\",\n round(total_kwh::numeric, 1) AS \"Total annonce (kWh)\",\n round((horaire[8] + horaire[9] + horaire[10] + horaire[11])::numeric, 2) AS \"07-11 h\",\n round((horaire[12] + horaire[13] + horaire[14] + horaire[15])::numeric, 2) AS \"11-15 h\",\n round((horaire[16] + horaire[17] + horaire[18] + horaire[19])::numeric, 2) AS \"15-19 h\"\nFROM prevision\nWHERE $__timeFilter(publie_a)\nORDER BY publie_a DESC, jour"
} }
], ],
"gridPos": { "gridPos": {

View File

@@ -74,7 +74,7 @@
"format": "time_series", "format": "time_series",
"rawQuery": true, "rawQuery": true,
"editorMode": "code", "editorMode": "code",
"rawSql": "SELECT ts AS time,\n max(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS \"Production\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Maison\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS \"Voiture\",\n max(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system')\n - max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Excedent\"\nFROM mesure\nWHERE $__timeFilter(ts)\nGROUP BY ts\nORDER BY ts" "rawSql": "WITH par_minute AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS Production,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS Maison,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS Voiture\n FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npaliers AS (\n SELECT t, Production, Maison, Voiture,\n count(Production) OVER (ORDER BY t) AS g_Production,\n count(Maison) OVER (ORDER BY t) AS g_Maison,\n count(Voiture) OVER (ORDER BY t) AS g_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n first_value(Production) OVER (PARTITION BY g_Production ORDER BY t) AS Production,\n first_value(Maison) OVER (PARTITION BY g_Maison ORDER BY t) AS Maison,\n first_value(Voiture) OVER (PARTITION BY g_Voiture ORDER BY t) AS Voiture\n FROM paliers\n)\nSELECT t AS time,\n Production AS \"Production\",\n Maison AS \"Maison\",\n Voiture AS \"Voiture\",\n Production - Maison AS \"Excedent\"\nFROM report ORDER BY t"
} }
], ],
"gridPos": { "gridPos": {
@@ -270,7 +270,7 @@
{ {
"type": "state-timeline", "type": "state-timeline",
"title": "Ce que les chargeurs declarent eux-memes", "title": "Ce que les chargeurs declarent eux-memes",
"description": "mppt_active = ce chargeur donne tout ce qu'il peut. voltage_current_limited ou off = il se retient, faute de preneur. Le 03/09 ces sept capteurs ont bascule 24 minutes avant le drapeau du bilan — c'est le signal le plus direct dont on dispose.", "description": "mppt_active = ce chargeur donne tout ce qu'il peut. voltage_current_limited ou off = il se retient, faute de preneur. Le 03/09 ces capteurs ont bascule 24 minutes avant le drapeau du bilan — c'est le signal le plus direct dont on dispose.",
"datasource": { "datasource": {
"type": "influxdb", "type": "influxdb",
"uid": "evcs-influx" "uid": "evcs-influx"
@@ -357,7 +357,7 @@
"format": "time_series", "format": "time_series",
"rawQuery": true, "rawQuery": true,
"editorMode": "code", "editorMode": "code",
"rawSql": "SELECT ts AS time,\n max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_soc') AS \"SOC\"\nFROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts" "rawSql": "SELECT date_trunc('minute', ts) AS time, avg(valeur) AS \"SOC\"\nFROM mesure WHERE cle = 'sensor.victron_battery_soc'\n AND $__timeFilter(ts) GROUP BY 1 ORDER BY 1"
}, },
{ {
"refId": "B", "refId": "B",
@@ -368,7 +368,7 @@
"format": "time_series", "format": "time_series",
"rawQuery": true, "rawQuery": true,
"editorMode": "code", "editorMode": "code",
"rawSql": "SELECT ts AS time,\n max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_current') AS \"Courant\"\nFROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts" "rawSql": "SELECT date_trunc('minute', ts) AS time, avg(valeur) AS \"Courant\"\nFROM mesure WHERE cle = 'sensor.victron_battery_current'\n AND $__timeFilter(ts) GROUP BY 1 ORDER BY 1"
} }
], ],
"gridPos": { "gridPos": {

View File

@@ -13,6 +13,43 @@ def sql(refid, q, fmt="time_series"):
def flux(refid, q): def flux(refid, q):
return {"refId": refid, "datasource": IN, "query": q} return {"refId": refid, "datasource": IN, "query": q}
def sql_grandeurs(refid, colonnes, calculees=None):
"""Rapproche plusieurs grandeurs de la table `mesure` sur un axe commun.
Deux difficultes, toutes deux resolues ici :
1. Le service horodate chaque mesure avec son propre `last_updated` — le
vrai instant de la mesure, ce qui est le bon choix. Mais deux grandeurs
ne partagent alors presque jamais le meme `ts` : un `GROUP BY ts` rend
une ligne par grandeur, avec une seule colonne remplie.
2. Le service n'ecrit une mesure que si elle a change. Un simple
regroupement a la minute laisse donc des trous, et toute colonne
CALCULEE (une difference, par exemple) n'existe que les minutes ou
toutes ses entrees ont bouge — 73 minutes sur 434 le 04/09.
D'ou le report de la derniere valeur connue, qui est aussi ce que fait le
controleur en temps reel : il decide sur la derniere valeur qu'il a lue.
L'astuce `count(x) OVER (ORDER BY t)` numerote les paliers entre deux
valeurs non nulles ; `first_value` reprend celle du debut du palier.
"""
noms = [n for _, n in colonnes]
par_minute = ",\n".join(
f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n}" for c, n in colonnes)
groupes = ",\n".join(f" count({n}) OVER (ORDER BY t) AS g_{n}" for n in noms)
remplis = ",\n".join(
f" first_value({n}) OVER (PARTITION BY g_{n} ORDER BY t) AS {n}" for n in noms)
sorties = ",\n".join(f' {n} AS "{n.replace("_", " ")}"' for n in noms)
if calculees:
sorties += ",\n" + ",\n".join(f' {e} AS "{lib}"' for e, lib in calculees)
q = (f"WITH par_minute AS (\n"
f" SELECT date_trunc('minute', ts) AS t,\n{par_minute}\n"
f" FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n"
f"),\npaliers AS (\n SELECT t, {', '.join(noms)},\n{groupes}\n FROM par_minute\n"
f"),\nreport AS (\n SELECT t,\n{remplis}\n FROM paliers\n)\n"
f"SELECT t AS time,\n{sorties}\nFROM report ORDER BY t")
return sql(refid, q)
def ts(title, targets, ds, x, y, w, h, unit="", desc="", extra=None, legend="bottom"): def ts(title, targets, ds, x, y, w, h, unit="", desc="", extra=None, legend="bottom"):
p = {"type": "timeseries", "title": title, "description": desc, p = {"type": "timeseries", "title": title, "description": desc,
"datasource": ds, "targets": targets, "datasource": ds, "targets": targets,
@@ -171,15 +208,11 @@ nuit = dashboard("evcs-nuit", "La nuit", (
"demande": {"color": "semi-dark-orange", "index": 2}}}]), "demande": {"color": "semi-dark-orange", "index": 2}}}]),
ts("Les puissances de la nuit", [ ts("Les puissances de la nuit", [
sql("A", "SELECT ts AS time,\n" sql_grandeurs("A", [
" max(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS \"Reseau\",\n" ('sensor.victron_grid_power_l1', 'Reseau'),
" max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_power') AS \"Batterie\",\n" ('sensor.victron_battery_power', 'Batterie'),
" max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Maison\",\n" ('sensor.victron_consumption_power_l1', 'Maison'),
" max(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS \"Voiture\"\n" ('sensor.victron_evcs_ac_power', 'Voiture')])],
"FROM mesure\n"
"WHERE $__timeFilter(ts)\n"
"GROUP BY ts\n"
"ORDER BY ts")],
PG, 0, 16, 12, 8, "watt", PG, 0, 16, 12, 8, "watt",
"Batterie positive = elle se charge. Contacteur ferme avec un import qui " "Batterie positive = elle se charge. Contacteur ferme avec un import qui "
"ne couvre pas la maison = la batterie paie quand meme : c'est le defaut " "ne couvre pas la maison = la batterie paie quand meme : c'est le defaut "
@@ -246,16 +279,11 @@ solaire = dashboard("evcs-solaire", "Le solaire", (
"Ces panneaux montrent l'ecart entre les deux."), [ "Ces panneaux montrent l'ecart entre les deux."), [
ts("Production, maison, et ce qui reste", [ ts("Production, maison, et ce qui reste", [
sql("A", "SELECT ts AS time,\n" sql_grandeurs("A", [
" max(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS \"Production\",\n" ('sensor.victron_pv_power_system', 'Production'),
" max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Maison\",\n" ('sensor.victron_consumption_power_l1', 'Maison'),
" max(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS \"Voiture\",\n" ('sensor.victron_evcs_ac_power', 'Voiture')],
" max(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system')\n" calculees=[('Production - Maison', 'Excedent')])],
" - max(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS \"Excedent\"\n"
"FROM mesure\n"
"WHERE $__timeFilter(ts)\n"
"GROUP BY ts\n"
"ORDER BY ts")],
PG, 0, 0, 24, 9, "watt", PG, 0, 0, 24, 9, "watt",
"L'excedent (production moins maison hors voiture) est la grandeur qui " "L'excedent (production moins maison hors voiture) est la grandeur qui "
"decide d'un demarrage depuis le 03/09. Le 03/09 a 19:44, la production " "decide d'un demarrage depuis le 03/09. Le 03/09 a 19:44, la production "
@@ -308,7 +336,7 @@ solaire = dashboard("evcs-solaire", "Le solaire", (
IN, 12, 9, 12, 6, IN, 12, 9, 12, 6,
"mppt_active = ce chargeur donne tout ce qu'il peut. " "mppt_active = ce chargeur donne tout ce qu'il peut. "
"voltage_current_limited ou off = il se retient, faute de preneur. " "voltage_current_limited ou off = il se retient, faute de preneur. "
"Le 03/09 ces sept capteurs ont bascule 24 minutes avant le drapeau du " "Le 03/09 ces capteurs ont bascule 24 minutes avant le drapeau du "
"bilan — c'est le signal le plus direct dont on dispose.", "bilan — c'est le signal le plus direct dont on dispose.",
mappings=[{"type": "value", "options": { mappings=[{"type": "value", "options": {
"mppt_active": {"color": "green", "index": 0}, "mppt_active": {"color": "green", "index": 0},
@@ -316,12 +344,12 @@ solaire = dashboard("evcs-solaire", "Le solaire", (
"off": {"color": "dark-blue", "index": 2}}}]), "off": {"color": "dark-blue", "index": 2}}}]),
ts("Batterie", [ ts("Batterie", [
sql("A", "SELECT ts AS time,\n" sql("A", "SELECT date_trunc('minute', ts) AS time, avg(valeur) AS \"SOC\"\n"
" max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_soc') AS \"SOC\"\n" "FROM mesure WHERE cle = 'sensor.victron_battery_soc'\n"
"FROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts"), " AND $__timeFilter(ts) GROUP BY 1 ORDER BY 1"),
sql("B", "SELECT ts AS time,\n" sql("B", "SELECT date_trunc('minute', ts) AS time, avg(valeur) AS \"Courant\"\n"
" max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_current') AS \"Courant\"\n" "FROM mesure WHERE cle = 'sensor.victron_battery_current'\n"
"FROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts")], " AND $__timeFilter(ts) GROUP BY 1 ORDER BY 1")],
PG, 0, 15, 12, 8, "percent", PG, 0, 15, 12, 8, "percent",
"Le SOC n'est pas lineaire : 0,169 kWh par point au-dessus de 33 %, " "Le SOC n'est pas lineaire : 0,169 kWh par point au-dessus de 33 %, "
"0,097 en dessous (576 paliers mesures). Un point en bas d'echelle vaut " "0,097 en dessous (576 paliers mesures). Un point en bas d'echelle vaut "
@@ -372,20 +400,29 @@ prevu AS (
horaire[h] AS kwh horaire[h] AS kwh
FROM du_matin, generate_subscripts(horaire, 1) AS h FROM du_matin, generate_subscripts(horaire, 1) AS h
), ),
produit AS ( -- INTEGRATION PONDEREE PAR LA DUREE, et non somme/60.
-- Un echantillon toutes les 60 s : chaque point vaut 1/60 h. -- Le service n'ecrit une mesure que lorsqu'elle change : compter les
SELECT date_trunc('hour', ts) AS heure, -- echantillons revient a supposer qu'il y en a 60 par heure alors qu'il y
sum(valeur) / 60.0 / 1000.0 AS kwh -- en a eu 26 a 47 le 04/09 : l'energie etait sous-estimee d'environ 9 %.
-- Chaque point vaut donc son intervalle jusqu'au suivant, plafonne a 300 s
-- pour qu'un arret du service n'extrapole pas une valeur perimee.
pas AS (
SELECT ts, valeur,
LEAST(EXTRACT(epoch FROM (lead(ts) OVER (ORDER BY ts) - ts)), 300) AS dt
FROM mesure FROM mesure
WHERE cle = 'sensor.victron_pv_power_system' AND $__timeFilter(ts) WHERE cle = 'sensor.victron_pv_power_system' AND $__timeFilter(ts)
GROUP BY 1 ),
produit AS (
SELECT date_trunc('hour', ts) AS heure,
sum(valeur * coalesce(dt, 0)) / 3600.0 / 1000.0 AS kwh
FROM pas GROUP BY 1
) )
SELECT coalesce(prevu.heure, produit.heure) AS time, SELECT to_char(coalesce(prevu.heure, produit.heure), 'DD/MM HH24') || ' h' AS "Heure",
round(prevu.kwh::numeric, 2) AS "Prevu le matin meme", round(prevu.kwh::numeric, 2) AS "Prevu le matin meme",
round(produit.kwh::numeric, 2) AS "Reellement produit" round(produit.kwh::numeric, 2) AS "Reellement produit"
FROM prevu FULL OUTER JOIN produit USING (heure) FROM prevu FULL OUTER JOIN produit USING (heure)
WHERE coalesce(prevu.heure, produit.heure) BETWEEN $__timeFrom() AND $__timeTo() WHERE coalesce(prevu.heure, produit.heure) BETWEEN $__timeFrom() AND $__timeTo()
ORDER BY 1""" ORDER BY coalesce(prevu.heure, produit.heure)"""
prevision = dashboard("evcs-prevision", "Prevision contre reel", ( prevision = dashboard("evcs-prevision", "Prevision contre reel", (
"Le total de la prevision tombe juste ; sa FORME non. Sur les trois jours " "Le total de la prevision tombe juste ; sa FORME non. Sur les trois jours "
@@ -414,7 +451,8 @@ prevision = dashboard("evcs-prevision", "Prevision contre reel", (
"bridee (mesure inexploitable)": {"color": "orange", "index": 1}}}]), "bridee (mesure inexploitable)": {"color": "orange", "index": 1}}}]),
table("Les previsions telles qu'elles ont ete publiees", [ table("Les previsions telles qu'elles ont ete publiees", [
sql("A", "SELECT publie_a AS \"Publiee le\", jour AS \"Pour le jour\",\n" sql("A", "SELECT publie_a AS \"Publiee le\",\n"
" to_char(jour, 'DD/MM') AS \"Pour le jour\",\n"
" round(total_kwh::numeric, 1) AS \"Total annonce (kWh)\",\n" " round(total_kwh::numeric, 1) AS \"Total annonce (kWh)\",\n"
" round((horaire[8] + horaire[9] + horaire[10] + horaire[11])::numeric, 2) AS \"07-11 h\",\n" " round((horaire[8] + horaire[9] + horaire[10] + horaire[11])::numeric, 2) AS \"07-11 h\",\n"
" round((horaire[12] + horaire[13] + horaire[14] + horaire[15])::numeric, 2) AS \"11-15 h\",\n" " round((horaire[12] + horaire[13] + horaire[14] + horaire[15])::numeric, 2) AS \"11-15 h\",\n"