diff --git a/deploy/historisation/grafana/dashboards/journee.json b/deploy/historisation/grafana/dashboards/journee.json index 8fdd2cf..73e6638 100644 --- a/deploy/historisation/grafana/dashboards/journee.json +++ b/deploy/historisation/grafana/dashboards/journee.json @@ -60,9 +60,9 @@ }, "panels": [ { - "type": "stat", + "type": "table", "title": "Le bilan depuis minuit", - "description": "Sept chiffres, une 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": "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.", "datasource": { "type": "grafana-postgresql-datasource", "uid": "evcs-pg" @@ -77,7 +77,7 @@ "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)\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)" + "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" } ], "gridPos": { @@ -88,163 +88,65 @@ }, "fieldConfig": { "defaults": { - "color": { - "mode": "thresholds" - }, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "text", - "value": null - } - ] - }, - "mappings": [] + "custom": { + "align": "left", + "cellOptions": { + "type": "auto" + }, + "filterable": true + } }, "overrides": [ { "matcher": { "id": "byName", - "options": "Solaire produit" + "options": "rang" }, "properties": [ { - "id": "unit", - "value": "kwatth" + "id": "custom.hidden", + "value": true } ] }, { "matcher": { "id": "byName", - "options": "Maison consommee" + "options": "Grandeur" }, "properties": [ { - "id": "unit", - "value": "kwatth" + "id": "custom.width", + "value": 230 } ] }, { "matcher": { "id": "byName", - "options": "Voiture" + "options": "Valeur" }, "properties": [ { - "id": "unit", - "value": "kwatth" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Batterie chargee" - }, - "properties": [ - { - "id": "unit", - "value": "kwatth" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Batterie rendue" - }, - "properties": [ - { - "id": "unit", - "value": "kwatth" - } - ] - }, - { - "matcher": { - "id": "byName", - "options": "Reseau importe" - }, - "properties": [ - { - "id": "unit", - "value": "kwatth" + "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": "thresholds", - "value": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "#EAB839", - "value": 60 - }, - { - "color": "green", - "value": 85 - } - ] - } + "id": "decimals", + "value": 1 } ] } ] }, "options": { - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "orientation": "horizontal", - "textMode": "auto", - "colorMode": "value", - "graphMode": "none", - "justifyMode": "center", - "percentChangeColorMode": "standard", - "noValue": "—", - "wideLayout": true + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } } }, { diff --git a/tools/gen-grafana-dashboards.py b/tools/gen-grafana-dashboards.py index 8fa6563..9cb49bc 100644 --- a/tools/gen-grafana-dashboards.py +++ b/tools/gen-grafana-dashboards.py @@ -1274,23 +1274,36 @@ 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 `GROUP BY (1 = 1)` n'est pas cosmetique. Un agregat -# GLOBAL (sans GROUP BY) sur ces CTE rendait un panneau vide — sept « — » — -# alors que la meme requete donnait les bons totaux en psql, que le fichier -# servi par Grafana etait bien le dernier (md5 verifie) et que le serveur ne -# journalisait aucune erreur. L'histogramme, qui partage exactement les memes -# CTE mais groupe par heure, s'affichait lui sans probleme : le GROUP BY etait -# la derniere difference structurelle entre les deux. -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)""" +# Le bilan sort en LIGNES (une par grandeur), pas en colonnes. +# +# ⚠️ 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""" BILAN_FLUX = BILAN_BASE + """SELECT t AS time, pv AS "Production solaire", @@ -1306,23 +1319,17 @@ journee = dashboard( "produit, ce qui a ete consomme, ce qui a ete achete, et ou c'est alle. " "Les energies sont integrees depuis les puissances mesurees.", [ - stat("Le bilan depuis minuit", [sql("A", BILAN_TOTAUX, fmt="table")], + table("Le bilan depuis minuit", [sql("A", BILAN_TOTAUX, fmt="table")], PG, 0, 0, 24, 5, - desc="Sept chiffres, une ligne. L'autonomie est la part de la " + desc="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.", - overrides=[sur(n, [{"id": "unit", "value": "kwatth"}]) for n in - ("Solaire produit", "Maison consommee", "Voiture", - "Batterie chargee", "Batterie rendue")] - + [sur("Reseau importe", [{"id": "unit", "value": "kwatth"}, - {"id": "thresholds", "value": seuils( - ("green", None), ("#EAB839", 3), - ("orange", 8))}]), - sur("Autonomie", [{"id": "unit", "value": "percent"}, - {"id": "thresholds", "value": seuils( - ("red", None), ("#EAB839", 60), - ("green", 85))}])]), + 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}])]), # ⚠️ 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