Le bilan se lit en colonnes : les titres, puis les valeurs

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
Tallagrand Thierry
2026-09-08 00:24:00 +02:00
parent 2569718ddd
commit c42b29c76f
2 changed files with 177 additions and 48 deletions

View File

@@ -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
}
]
}
}
]
}

View File

@@ -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