Le bilan devient un tableau, parce que le panneau stat ne le rendait pas

Quatre tentatives sur le panneau stat : selecteur de champs, unites, bornes de
l'axe, GROUP BY, retrait de la colonne texte et de son override. A chaque fois
sept « — » a l'ecran.

L'acces a l'API Grafana, obtenu entre-temps, a tranche : /api/ds/query montre
que le serveur renvoyait bien sept champs de type number avec leurs valeurs, y
compris sur la fenetre exacte de la derniere capture. Ce n'etait donc ni la
requete, ni le cache du navigateur, ni le provisioning — md5 du fichier servi
et version en base verifies. Le panneau recevait les donnees et ne les rendait
pas.

Le bilan sort desormais en LIGNES, une par grandeur, dans un panneau table :
celui-ci affiche toujours ce qu'il recoit, et le format se lit tres bien pour un
bilan. L'unite est portee par le libelle, ce qui permet de melanger kWh et
pourcentage dans la meme colonne.

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:19:46 +02:00
parent c387415c4f
commit 2569718ddd
2 changed files with 65 additions and 156 deletions

View File

@@ -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
}
}
},
{

View File

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