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:
@@ -13,6 +13,43 @@ def sql(refid, q, fmt="time_series"):
|
||||
def flux(refid, 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"):
|
||||
p = {"type": "timeseries", "title": title, "description": desc,
|
||||
"datasource": ds, "targets": targets,
|
||||
@@ -171,15 +208,11 @@ nuit = dashboard("evcs-nuit", "La nuit", (
|
||||
"demande": {"color": "semi-dark-orange", "index": 2}}}]),
|
||||
|
||||
ts("Les puissances de la nuit", [
|
||||
sql("A", "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\"\n"
|
||||
"FROM mesure\n"
|
||||
"WHERE $__timeFilter(ts)\n"
|
||||
"GROUP BY ts\n"
|
||||
"ORDER BY ts")],
|
||||
sql_grandeurs("A", [
|
||||
('sensor.victron_grid_power_l1', 'Reseau'),
|
||||
('sensor.victron_battery_power', 'Batterie'),
|
||||
('sensor.victron_consumption_power_l1', 'Maison'),
|
||||
('sensor.victron_evcs_ac_power', 'Voiture')])],
|
||||
PG, 0, 16, 12, 8, "watt",
|
||||
"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 "
|
||||
@@ -246,16 +279,11 @@ solaire = dashboard("evcs-solaire", "Le solaire", (
|
||||
"Ces panneaux montrent l'ecart entre les deux."), [
|
||||
|
||||
ts("Production, maison, et ce qui reste", [
|
||||
sql("A", "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\"\n"
|
||||
"FROM mesure\n"
|
||||
"WHERE $__timeFilter(ts)\n"
|
||||
"GROUP BY ts\n"
|
||||
"ORDER BY ts")],
|
||||
sql_grandeurs("A", [
|
||||
('sensor.victron_pv_power_system', 'Production'),
|
||||
('sensor.victron_consumption_power_l1', 'Maison'),
|
||||
('sensor.victron_evcs_ac_power', 'Voiture')],
|
||||
calculees=[('Production - Maison', 'Excedent')])],
|
||||
PG, 0, 0, 24, 9, "watt",
|
||||
"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 "
|
||||
@@ -308,7 +336,7 @@ solaire = dashboard("evcs-solaire", "Le solaire", (
|
||||
IN, 12, 9, 12, 6,
|
||||
"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 "
|
||||
"Le 03/09 ces capteurs ont bascule 24 minutes avant le drapeau du "
|
||||
"bilan — c'est le signal le plus direct dont on dispose.",
|
||||
mappings=[{"type": "value", "options": {
|
||||
"mppt_active": {"color": "green", "index": 0},
|
||||
@@ -316,12 +344,12 @@ solaire = dashboard("evcs-solaire", "Le solaire", (
|
||||
"off": {"color": "dark-blue", "index": 2}}}]),
|
||||
|
||||
ts("Batterie", [
|
||||
sql("A", "SELECT ts AS time,\n"
|
||||
" max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_soc') AS \"SOC\"\n"
|
||||
"FROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts"),
|
||||
sql("B", "SELECT ts AS time,\n"
|
||||
" max(valeur) FILTER (WHERE cle = 'sensor.victron_battery_current') AS \"Courant\"\n"
|
||||
"FROM mesure WHERE $__timeFilter(ts) GROUP BY ts ORDER BY ts")],
|
||||
sql("A", "SELECT date_trunc('minute', ts) AS time, avg(valeur) AS \"SOC\"\n"
|
||||
"FROM mesure WHERE cle = 'sensor.victron_battery_soc'\n"
|
||||
" AND $__timeFilter(ts) GROUP BY 1 ORDER BY 1"),
|
||||
sql("B", "SELECT date_trunc('minute', ts) AS time, avg(valeur) AS \"Courant\"\n"
|
||||
"FROM mesure WHERE cle = 'sensor.victron_battery_current'\n"
|
||||
" AND $__timeFilter(ts) GROUP BY 1 ORDER BY 1")],
|
||||
PG, 0, 15, 12, 8, "percent",
|
||||
"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 "
|
||||
@@ -372,20 +400,29 @@ prevu AS (
|
||||
horaire[h] AS kwh
|
||||
FROM du_matin, generate_subscripts(horaire, 1) AS h
|
||||
),
|
||||
produit AS (
|
||||
-- Un echantillon toutes les 60 s : chaque point vaut 1/60 h.
|
||||
SELECT date_trunc('hour', ts) AS heure,
|
||||
sum(valeur) / 60.0 / 1000.0 AS kwh
|
||||
-- INTEGRATION PONDEREE PAR LA DUREE, et non somme/60.
|
||||
-- Le service n'ecrit une mesure que lorsqu'elle change : compter les
|
||||
-- echantillons revient a supposer qu'il y en a 60 par heure alors qu'il y
|
||||
-- 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
|
||||
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,
|
||||
round(prevu.kwh::numeric, 2) AS "Prevu le matin meme",
|
||||
round(produit.kwh::numeric, 2) AS "Reellement produit"
|
||||
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(produit.kwh::numeric, 2) AS "Reellement produit"
|
||||
FROM prevu FULL OUTER JOIN produit USING (heure)
|
||||
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", (
|
||||
"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}}}]),
|
||||
|
||||
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((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"
|
||||
|
||||
Reference in New Issue
Block a user