From c42b29c76fd655ad493839e5fa6999cf539e2533 Mon Sep 17 00:00:00 2001 From: Tallagrand Thierry Date: Tue, 8 Sep 2026 00:24:00 +0200 Subject: [PATCH] Le bilan se lit en colonnes : les titres, puis les valeurs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le format « une ligne par grandeur » etait illisible, et la hauteur du panneau n'en montrait que trois sur sept. Le tableau rend desormais une ligne d'en-tetes et une ligne de valeurs, ce qui est la forme attendue pour un resume. C'est toujours un panneau `table` et pas un `stat` : ce dernier, sur exactement cette requete, affichait sept « — » alors que l'API montrait qu'il recevait sept champs de type number avec leurs valeurs. Une note le rappelle dans le generateur pour ne pas y revenir. Les unites reviennent en overrides puisque les libelles n'ont plus a les porter, avec deux colorations utiles : le reseau importe passe a l'ambre au-dela de 3 kWh et a l'orange au-dela de 8, l'autonomie a un fond vert au-dessus de 85 %. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj --- .../grafana/dashboards/journee.json | 144 ++++++++++++++++-- tools/gen-grafana-dashboards.py | 81 +++++----- 2 files changed, 177 insertions(+), 48 deletions(-) diff --git a/deploy/historisation/grafana/dashboards/journee.json b/deploy/historisation/grafana/dashboards/journee.json index 73e6638..1f946ac 100644 --- a/deploy/historisation/grafana/dashboards/journee.json +++ b/deploy/historisation/grafana/dashboards/journee.json @@ -62,7 +62,7 @@ { "type": "table", "title": "Le bilan depuis minuit", - "description": "Sept grandeurs, une par ligne. L'autonomie est la part de la consommation de la maison qui n'est PAS venue du reseau. « Batterie rendue » est ce qu'elle a restitue, donc du solaire differe et non une consommation supplementaire.", + "description": "Une ligne d'en-tetes, une ligne de valeurs. L'autonomie est la part de la consommation de la maison qui n'est PAS venue du reseau. « Batterie rendue » est ce qu'elle a restitue, donc du solaire differe et non une consommation supplementaire.", "datasource": { "type": "grafana-postgresql-datasource", "uid": "evcs-pg" @@ -77,14 +77,14 @@ "format": "table", "rawQuery": true, "editorMode": "code", - "rawSql": "WITH bornes AS (\n SELECT min(ts) AS d, max(ts) AS f FROM mesure WHERE $__timeFilter(ts)\n),\naxe AS (\n SELECT generate_series(date_trunc('minute', (SELECT d FROM bornes)),\n date_trunc('minute', (SELECT f FROM bornes)),\n interval '1 minute') AS t\n),\nreleve AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS pv,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_pv_power_system'))[1] AS d_pv,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS maison,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1'))[1] AS d_maison,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS reseau,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_grid_power_l1'))[1] AS d_reseau,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS voiture,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power'))[1] AS d_voiture,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_battery_power') AS batterie,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_battery_power'))[1] AS d_batterie\n FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npar_minute AS (\n SELECT axe.t, releve.pv, releve.d_pv, releve.maison, releve.d_maison, releve.reseau, releve.d_reseau, releve.voiture, releve.d_voiture, releve.batterie, releve.d_batterie\n FROM axe LEFT JOIN releve USING (t)\n),\npaliers AS (\n SELECT t, pv, d_pv, maison, d_maison, reseau, d_reseau, voiture, d_voiture, batterie, d_batterie,\n count(d_pv) OVER (ORDER BY t) AS g_pv,\n count(d_maison) OVER (ORDER BY t) AS g_maison,\n count(d_reseau) OVER (ORDER BY t) AS g_reseau,\n count(d_voiture) OVER (ORDER BY t) AS g_voiture,\n count(d_batterie) OVER (ORDER BY t) AS g_batterie\n FROM par_minute\n),\nreport AS (\n SELECT t,\n coalesce(pv, first_value(d_pv) OVER (PARTITION BY g_pv ORDER BY t), 0) AS pv,\n coalesce(maison, first_value(d_maison) OVER (PARTITION BY g_maison ORDER BY t), 0) AS maison,\n coalesce(reseau, first_value(d_reseau) OVER (PARTITION BY g_reseau ORDER BY t), 0) AS reseau,\n coalesce(voiture, first_value(d_voiture) OVER (PARTITION BY g_voiture ORDER BY t), 0) AS voiture,\n coalesce(batterie, first_value(d_batterie) OVER (PARTITION BY g_batterie ORDER BY t), 0) AS batterie\n FROM paliers\n)\n,\ntotaux AS (\n SELECT sum(pv) AS pv, sum(maison) AS maison, sum(reseau) AS reseau,\n sum(voiture) AS voiture,\n sum(greatest(batterie, 0)) AS bat_plus,\n sum(greatest(-batterie, 0)) AS bat_moins\n FROM report\n)\nSELECT x.rang, x.grandeur AS \"Grandeur\", x.valeur AS \"Valeur\"\nFROM totaux, LATERAL (VALUES\n (1, 'Solaire produit (kWh)', round((pv / 60.0 / 1000.0)::numeric, 1)),\n (2, 'Maison consommee (kWh)', round((maison / 60.0 / 1000.0)::numeric, 1)),\n (3, 'Reseau importe (kWh)', round((reseau / 60.0 / 1000.0)::numeric, 1)),\n (4, 'Voiture (kWh)', round((voiture / 60.0 / 1000.0)::numeric, 1)),\n (5, 'Batterie chargee (kWh)', round((bat_plus / 60.0 / 1000.0)::numeric, 1)),\n (6, 'Batterie rendue (kWh)', round((bat_moins/ 60.0 / 1000.0)::numeric, 1)),\n (7, 'Autonomie (%)', round((100.0 * greatest(maison - reseau, 0)\n / nullif(maison, 0))::numeric, 0))\n) AS x(rang, grandeur, valeur)\nORDER BY x.rang" + "rawSql": "WITH bornes AS (\n SELECT min(ts) AS d, max(ts) AS f FROM mesure WHERE $__timeFilter(ts)\n),\naxe AS (\n SELECT generate_series(date_trunc('minute', (SELECT d FROM bornes)),\n date_trunc('minute', (SELECT f FROM bornes)),\n interval '1 minute') AS t\n),\nreleve AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS pv,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_pv_power_system'))[1] AS d_pv,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS maison,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1'))[1] AS d_maison,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS reseau,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_grid_power_l1'))[1] AS d_reseau,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS voiture,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power'))[1] AS d_voiture,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_battery_power') AS batterie,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_battery_power'))[1] AS d_batterie\n FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npar_minute AS (\n SELECT axe.t, releve.pv, releve.d_pv, releve.maison, releve.d_maison, releve.reseau, releve.d_reseau, releve.voiture, releve.d_voiture, releve.batterie, releve.d_batterie\n FROM axe LEFT JOIN releve USING (t)\n),\npaliers AS (\n SELECT t, pv, d_pv, maison, d_maison, reseau, d_reseau, voiture, d_voiture, batterie, d_batterie,\n count(d_pv) OVER (ORDER BY t) AS g_pv,\n count(d_maison) OVER (ORDER BY t) AS g_maison,\n count(d_reseau) OVER (ORDER BY t) AS g_reseau,\n count(d_voiture) OVER (ORDER BY t) AS g_voiture,\n count(d_batterie) OVER (ORDER BY t) AS g_batterie\n FROM par_minute\n),\nreport AS (\n SELECT t,\n coalesce(pv, first_value(d_pv) OVER (PARTITION BY g_pv ORDER BY t), 0) AS pv,\n coalesce(maison, first_value(d_maison) OVER (PARTITION BY g_maison ORDER BY t), 0) AS maison,\n coalesce(reseau, first_value(d_reseau) OVER (PARTITION BY g_reseau ORDER BY t), 0) AS reseau,\n coalesce(voiture, first_value(d_voiture) OVER (PARTITION BY g_voiture ORDER BY t), 0) AS voiture,\n coalesce(batterie, first_value(d_batterie) OVER (PARTITION BY g_batterie ORDER BY t), 0) AS batterie\n FROM paliers\n)\nSELECT\n round((sum(pv) / 60.0 / 1000.0)::numeric, 1) AS \"Solaire produit\",\n round((sum(maison) / 60.0 / 1000.0)::numeric, 1) AS \"Maison consommee\",\n round((sum(reseau) / 60.0 / 1000.0)::numeric, 1) AS \"Reseau importe\",\n round((sum(voiture) / 60.0 / 1000.0)::numeric, 1) AS \"Voiture\",\n round((sum(greatest(batterie, 0)) / 60.0 / 1000.0)::numeric, 1) AS \"Batterie chargee\",\n round((sum(greatest(-batterie, 0)) / 60.0 / 1000.0)::numeric, 1) AS \"Batterie rendue\",\n round((100.0 * greatest(sum(maison) - sum(reseau), 0)\n / nullif(sum(maison), 0))::numeric, 0) AS \"Autonomie\"\nFROM report GROUP BY (1 = 1)" } ], "gridPos": { "x": 0, "y": 0, "w": 24, - "h": 5 + "h": 4 }, "fieldConfig": { "defaults": { @@ -100,42 +100,164 @@ { "matcher": { "id": "byName", - "options": "rang" + "options": "Solaire produit" }, "properties": [ { - "id": "custom.hidden", - "value": true + "id": "unit", + "value": "kwatth" + }, + { + "id": "decimals", + "value": 1 } ] }, { "matcher": { "id": "byName", - "options": "Grandeur" + "options": "Maison consommee" }, "properties": [ { - "id": "custom.width", - "value": 230 + "id": "unit", + "value": "kwatth" + }, + { + "id": "decimals", + "value": 1 } ] }, { "matcher": { "id": "byName", - "options": "Valeur" + "options": "Voiture" }, "properties": [ + { + "id": "unit", + "value": "kwatth" + }, + { + "id": "decimals", + "value": 1 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Batterie chargee" + }, + "properties": [ + { + "id": "unit", + "value": "kwatth" + }, + { + "id": "decimals", + "value": 1 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Batterie rendue" + }, + "properties": [ + { + "id": "unit", + "value": "kwatth" + }, + { + "id": "decimals", + "value": 1 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Reseau importe" + }, + "properties": [ + { + "id": "unit", + "value": "kwatth" + }, + { + "id": "decimals", + "value": 1 + }, { "id": "custom.cellOptions", "value": { "type": "color-text" } }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 3 + }, + { + "color": "orange", + "value": 8 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Autonomie" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, { "id": "decimals", - "value": 1 + "value": 0 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background" + } + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "#EAB839", + "value": 60 + }, + { + "color": "green", + "value": 85 + } + ] + } } ] } diff --git a/tools/gen-grafana-dashboards.py b/tools/gen-grafana-dashboards.py index 9cb49bc..4c36f8e 100644 --- a/tools/gen-grafana-dashboards.py +++ b/tools/gen-grafana-dashboards.py @@ -1274,36 +1274,30 @@ report AS ( # Une seule ligne, six colonnes : c'est le resume que l'on lit en premier. # L'autonomie se calcule sur la maison SANS la voiture, sinon une grosse charge # solaire du vehicule gonfle artificiellement le taux. -# Le bilan sort en LIGNES (une par grandeur), pas en colonnes. +# Le bilan sort en COLONNES : le tableau rend une ligne d'en-tetes et une ligne +# de valeurs, ce qui est la forme la plus lisible pour un resume. # -# ⚠️ Quatre tentatives ont echoue avec un panneau `stat` alimente par une seule -# ligne a sept colonnes : il affichait sept « — » alors que l'API `/api/ds/query` -# montrait que le serveur lui renvoyait bien sept champs de type number avec -# leurs valeurs. Ce n'etait donc ni la requete, ni le cache, ni le provisioning -# (md5 du fichier servi et version en base verifies) : le panneau recevait les -# donnees et ne les rendait pas. Un tableau, lui, affiche toujours ce qu'il -# recoit — et le format « une ligne par grandeur » se lit tres bien pour un -# bilan. L'unite est portee par le libelle, ce qui permet de melanger kWh et %. -BILAN_TOTAUX = BILAN_BASE + """, -totaux AS ( - SELECT sum(pv) AS pv, sum(maison) AS maison, sum(reseau) AS reseau, - sum(voiture) AS voiture, - sum(greatest(batterie, 0)) AS bat_plus, - sum(greatest(-batterie, 0)) AS bat_moins - FROM report -) -SELECT x.rang, x.grandeur AS "Grandeur", x.valeur AS "Valeur" -FROM totaux, LATERAL (VALUES - (1, 'Solaire produit (kWh)', round((pv / 60.0 / 1000.0)::numeric, 1)), - (2, 'Maison consommee (kWh)', round((maison / 60.0 / 1000.0)::numeric, 1)), - (3, 'Reseau importe (kWh)', round((reseau / 60.0 / 1000.0)::numeric, 1)), - (4, 'Voiture (kWh)', round((voiture / 60.0 / 1000.0)::numeric, 1)), - (5, 'Batterie chargee (kWh)', round((bat_plus / 60.0 / 1000.0)::numeric, 1)), - (6, 'Batterie rendue (kWh)', round((bat_moins/ 60.0 / 1000.0)::numeric, 1)), - (7, 'Autonomie (%)', round((100.0 * greatest(maison - reseau, 0) - / nullif(maison, 0))::numeric, 0)) -) AS x(rang, grandeur, valeur) -ORDER BY x.rang""" +# ⚠️ C'est un panneau `table`, PAS un `stat`. Quatre tentatives avec un `stat` +# sur exactement cette requete ont affiche sept « — » alors que l'API +# `/api/ds/query` montrait que le serveur lui renvoyait bien sept champs de type +# number avec leurs valeurs. Ce n'etait ni la requete, ni le cache, ni le +# provisioning (md5 du fichier servi et version en base verifies) : le panneau +# recevait les donnees et ne les rendait pas. Ne pas repasser en `stat`. +# +# ⚠️ Le `GROUP BY (1 = 1)` reste necessaire — un agregat global sans GROUP BY +# rendait un resultat vide cote Grafana. PostgreSQL refuse `GROUP BY true` +# (non-integer constant) mais accepte cette forme, qui n'impose aucune colonne +# dans le SELECT. +BILAN_TOTAUX = BILAN_BASE + """SELECT + round((sum(pv) / 60.0 / 1000.0)::numeric, 1) AS "Solaire produit", + round((sum(maison) / 60.0 / 1000.0)::numeric, 1) AS "Maison consommee", + round((sum(reseau) / 60.0 / 1000.0)::numeric, 1) AS "Reseau importe", + round((sum(voiture) / 60.0 / 1000.0)::numeric, 1) AS "Voiture", + round((sum(greatest(batterie, 0)) / 60.0 / 1000.0)::numeric, 1) AS "Batterie chargee", + round((sum(greatest(-batterie, 0)) / 60.0 / 1000.0)::numeric, 1) AS "Batterie rendue", + round((100.0 * greatest(sum(maison) - sum(reseau), 0) + / nullif(sum(maison), 0))::numeric, 0) AS "Autonomie" +FROM report GROUP BY (1 = 1)""" BILAN_FLUX = BILAN_BASE + """SELECT t AS time, pv AS "Production solaire", @@ -1320,16 +1314,29 @@ journee = dashboard( "Les energies sont integrees depuis les puissances mesurees.", [ table("Le bilan depuis minuit", [sql("A", BILAN_TOTAUX, fmt="table")], - PG, 0, 0, 24, 5, - desc="Sept grandeurs, une par ligne. L'autonomie est la part de la " - "consommation de la maison qui n'est PAS venue du reseau. " + PG, 0, 0, 24, 4, + desc="Une ligne d'en-tetes, une ligne de valeurs. L'autonomie est la part " + "de la consommation de la maison qui n'est PAS venue du reseau. " "« Batterie rendue » est ce qu'elle a restitue, donc du solaire " "differe et non une consommation supplementaire.", - overrides=[sur("rang", [{"id": "custom.hidden", "value": True}]), - sur("Grandeur", [{"id": "custom.width", "value": 230}]), - sur("Valeur", [{"id": "custom.cellOptions", - "value": {"type": "color-text"}}, - {"id": "decimals", "value": 1}])]), + overrides=[sur(n, [{"id": "unit", "value": "kwatth"}, + {"id": "decimals", "value": 1}]) for n in + ("Solaire produit", "Maison consommee", "Voiture", + "Batterie chargee", "Batterie rendue")] + + [sur("Reseau importe", [{"id": "unit", "value": "kwatth"}, + {"id": "decimals", "value": 1}, + {"id": "custom.cellOptions", + "value": {"type": "color-text"}}, + {"id": "thresholds", "value": seuils( + ("green", None), ("#EAB839", 3), + ("orange", 8))}]), + sur("Autonomie", [{"id": "unit", "value": "percent"}, + {"id": "decimals", "value": 0}, + {"id": "custom.cellOptions", + "value": {"type": "color-background"}}, + {"id": "thresholds", "value": seuils( + ("red", None), ("#EAB839", 60), + ("green", 85))}])]), # ⚠️ PAS de FILTRE_VUE ici : cette transformation ne garde que les colonnes # nommees par la variable $vue de « Le systeme ». Sur un tableau qui n'a pas