La prevision entre dans « La journee » et sur le telephone
Suite du correctif de l onglet Home Assistant. « La journee » avait le meme angle mort : cadre sur le jour civil, il s arretait a l heure en cours. Fenetre portee a la journee ENTIERE (dashboard() gagne un parametre `jusqu`), serie « Encore attendu » en gris translucide a la suite des barres mesurees, et colonne « Reste attendu » dans le bandeau du haut, a cote de « Solaire produit ». Sur le telephone, aucun panneau ajoute — un panneau de plus y est un ecran de plus a faire defiler. « Soleil restant » rejoint le bandeau Maintenant, a cote de Excedent ; « Soleil demain » rejoint La nuit qui vient, puisque c est lui qui decide jusqu ou la batterie peut descendre. Trois pieges, tous dans la meme direction. La mesure ne doit pas suivre la fenetre dans le futur : ENERGIE_SQL construisait son axe entre les deux macros puis reportait la derniere valeur connue, ce qui aurait rempli les heures a venir avec la puissance de l instant et gonfle l energie du jour — sa borne haute est desormais least($__timeTo(), now()). FULL OUTER JOIN et non LEFT : une periode a venir n a aucune ligne de mesure. Et borne haute stricte, sans quoi une barre « 18/09 00 h » apparaissait dans La journee. L heure EN COURS n a volontairement pas de barre de prevision : elle est deja mesuree pour ses minutes ecoulees. 73/73 requetes valides — mais ce controle prouve l execution, pas la grandeur. Les deux requetes de La journee ont donc ete rejouees a la main avec la vraie fenetre : 24 lignes, mesures jusqu a 02 h, prevision de 03 h a 23 h, somme des barres 36,95 kWh contre 37,0 annonces par le bandeau. C est ce recoupement qui a fait apparaitre le troisieme piege. Corrige au passage la note du 08/09 sur l acces a l API Grafana : les apostrophes du .env sont SIMPLES et non doubles, verifie a l octet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
82
HANDOFF.md
82
HANDOFF.md
@@ -5614,9 +5614,28 @@ tels quels a `curl`, d'ou le 401. Retirer les guillemets encadrants suffit :
|
||||
|
||||
```sh
|
||||
V=$(sudo sed -n 's/^GRAFANA_ADMIN_PASSWORD=//p' .env | tr -d '\r\n')
|
||||
PW=$(printf '%s' "$V" | sed -e 's/^"//' -e 's/"$//')
|
||||
PW=$(printf '%s' "$V" | sed -e "s/^['\"]//" -e "s/['\"]$//")
|
||||
```
|
||||
|
||||
⚠️ **CORRECTION du 17/09/2026 : ce sont des apostrophes SIMPLES, pas des
|
||||
guillemets doubles** — verifie a l'octet (`od -c` sur le premier et le dernier
|
||||
caractere). La commande ci-dessus a ete elargie aux deux formes ; celle que
|
||||
j'avais ecrite le 08/09 ne retirait rien et rendait donc toujours 401.
|
||||
|
||||
⚠️ **ET GRAFANA BLOQUE LE COMPTE APRES QUELQUES ECHECS** :
|
||||
`too many consecutive incorrect login attempts for user - login for user
|
||||
temporarily blocked`. En sondant le mot de passe le 17/09 j'ai verrouille
|
||||
`admin` pour quelques minutes — **le proprietaire ne pouvait plus se
|
||||
connecter a son interface non plus**. Ne jamais essayer un mot de passe en
|
||||
boucle sur cette API : extraire, verifier la longueur, essayer UNE fois.
|
||||
Consequence pratique : une fois le compte bloque, un mot de passe CORRECT est
|
||||
lui aussi refuse, donc l'echec ne prouve plus rien.
|
||||
|
||||
**Et l'API n'est pas necessaire pour verifier un tableau provisionne** : le
|
||||
fournisseur lit les FICHIERS montes (`updateIntervalSeconds: 30`), pas l'API.
|
||||
Il suffit donc de controler le contenu du fichier vu de l'interieur du
|
||||
conteneur et l'absence de ligne `provision` en erreur dans son journal.
|
||||
|
||||
L'API repond alors du premier coup. Interroger un panneau se fait par
|
||||
`/api/ds/query` depuis l'interieur du conteneur `evcs-grafana` :
|
||||
|
||||
@@ -6951,3 +6970,64 @@ la confiance dans la detection.**
|
||||
|
||||
---
|
||||
*Document mis a jour le : 17/09/2026 (~02:20) — chien de garde : `startedAt` absent lu comme une panne, valeur de repos divergente, confirmation sur deux cycles*
|
||||
|
||||
## 71. Session du 17/09/2026 — La prevision entre dans les tableaux Grafana
|
||||
|
||||
Suite du correctif de l'onglet « Prevision vs reel » de Home Assistant : le
|
||||
proprietaire a choisi **« La journee » d'abord, puis la ligne sur le
|
||||
telephone**.
|
||||
|
||||
### « La journee » (`evcs-journee`)
|
||||
Ce tableau avait exactement le meme angle mort : cadre sur `now/d`, il
|
||||
s'arretait a l'heure en cours. Une journee dont on ne montre que la moitie
|
||||
ecoulee ne repond pas a la question qu'on se pose en l'ouvrant a 10 h.
|
||||
|
||||
- Fenetre portee a **`now/d` → `now/d`** (la journee ENTIERE, fin du jour
|
||||
comprise). `dashboard()` gagne un parametre `jusqu`.
|
||||
- Le graphe horaire gagne une serie **« Encore attendu »**, gris translucide,
|
||||
a la suite des barres mesurees.
|
||||
- Le bandeau du haut gagne une colonne **« Reste attendu »**, a cote de
|
||||
« Solaire produit ». Deux chiffres cote a cote : ce qui est fait, ce qui vient.
|
||||
|
||||
### « Sur le telephone » (`evcs-telephone`)
|
||||
Aucun panneau ajoute — la contrainte de ce tableau est qu'un panneau de plus
|
||||
est un ecran de plus a faire defiler. Les deux chiffres rejoignent donc des
|
||||
bandeaux existants, chacun la ou il sert :
|
||||
- **« Soleil restant »** dans *Maintenant*, a cote de « Excedent » : ce qui
|
||||
reste a capter aujourd'hui ;
|
||||
- **« Soleil demain »** dans *La nuit qui vient* : c'est lui qui decide jusqu'ou
|
||||
la batterie peut descendre cette nuit.
|
||||
|
||||
Releve au deploiement : 60 % de batterie, marge +18,6, **37,0 kWh restants
|
||||
aujourd'hui**, 4,6 kWh a couvrir, relais du solaire a 09 h, **28,4 kWh annonces
|
||||
demain**.
|
||||
|
||||
### ⚠️ TROIS PIEGES RENCONTRES, tous dans la meme direction
|
||||
1. **La mesure ne doit pas suivre la fenetre dans le futur.** `ENERGIE_SQL`
|
||||
construisait son axe entre `$__timeFrom()` et `$__timeTo()` puis reportait la
|
||||
derniere valeur connue. Etendre la fenetre a la fin du jour aurait rempli les
|
||||
heures a venir avec la puissance de l'instant et **gonfle l'energie du jour**.
|
||||
Sa borne haute est desormais `least($__timeTo(), now())`.
|
||||
`BILAN_BASE` etait deja immunise : il derive ses bornes de `min(ts)/max(ts)`
|
||||
des donnees, pas des macros.
|
||||
2. **`FULL OUTER JOIN`, pas `LEFT`.** Une periode a venir n'a AUCUNE ligne de
|
||||
mesure et une periode passee aucune prevision ; un `LEFT JOIN` depuis la
|
||||
mesure aurait fait disparaitre toutes les barres de prevision.
|
||||
3. **Borne haute STRICTE.** Avec `<=`, « La journee » affichait une barre
|
||||
`18/09 00 h`. Une periode qui commence pile a la fin de la fenetre appartient
|
||||
a la suivante.
|
||||
|
||||
Et une regle de contenu : **l'heure EN COURS n'a pas de barre de prevision**.
|
||||
Elle est deja mesuree pour ses minutes ecoulees ; y superposer une prevision
|
||||
d'heure pleine ferait compter deux fois la meme heure.
|
||||
|
||||
### Verification
|
||||
`verifier-dashboards.mjs` : 73/73. Mais — troisieme rappel — il prouve qu'une
|
||||
requete s'EXECUTE, pas qu'elle rend la bonne grandeur. Les deux requetes de
|
||||
« La journee » ont donc ete rejouees a la main avec la vraie fenetre
|
||||
(`date_trunc('day', now())` → +1 jour) : 24 lignes, mesures jusqu'a 02 h,
|
||||
prevision de 03 h a 23 h, somme des barres 36,95 kWh contre 37,0 annonces par
|
||||
le bandeau. C'est ce recoupement qui a fait apparaitre le piege n°3.
|
||||
|
||||
---
|
||||
*Document mis a jour le : 17/09/2026 (~02:40) — prevision dans « La journee » et sur le telephone, 73/73*
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1245,7 +1245,7 @@
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"editorMode": "code",
|
||||
"rawSql": "WITH bornes AS (\n SELECT $__timeFrom()::timestamptz AS d, $__timeTo()::timestamptz AS f\n),\n-- Le pas s'adapte a la plage choisie. Sans cela, « 30 derniers jours » rend\n-- 720 barres illisibles — c'est exactement ce que fait VRM en passant de\n-- l'heure au jour puis au mois.\npas AS (\n SELECT CASE WHEN f - d > interval '70 days' THEN 'month'\n WHEN f - d > interval '4 days' THEN 'day'\n ELSE 'hour' END AS unite,\n CASE WHEN f - d > interval '70 days' THEN 'MM/YYYY'\n WHEN f - d > interval '4 days' THEN 'DD/MM'\n ELSE 'DD/MM HH24' END AS forme\n FROM bornes\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 Solaire,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_pv_power_system'))[1] AS d_Solaire,\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_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_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 FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npar_minute AS (\n SELECT axe.t, releve.Solaire, releve.d_Solaire, releve.Reseau, releve.d_Reseau, releve.Maison, releve.d_Maison, releve.Voiture, releve.d_Voiture\n FROM axe LEFT JOIN releve USING (t)\n),\npaliers AS (\n SELECT t, Solaire, d_Solaire, Reseau, d_Reseau, Maison, d_Maison, Voiture, d_Voiture,\n count(d_Solaire) OVER (ORDER BY t) AS g_Solaire,\n count(d_Reseau) OVER (ORDER BY t) AS g_Reseau,\n count(d_Maison) OVER (ORDER BY t) AS g_Maison,\n count(d_Voiture) OVER (ORDER BY t) AS g_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n coalesce(Solaire, first_value(d_Solaire) OVER (PARTITION BY g_Solaire ORDER BY t)) AS Solaire,\n coalesce(Reseau, first_value(d_Reseau) OVER (PARTITION BY g_Reseau ORDER BY t)) AS Reseau,\n coalesce(Maison, first_value(d_Maison) OVER (PARTITION BY g_Maison ORDER BY t)) AS Maison,\n coalesce(Voiture, first_value(d_Voiture) OVER (PARTITION BY g_Voiture ORDER BY t)) AS Voiture\n FROM paliers\n)\nSELECT to_char(date_trunc(pas.unite, t), pas.forme) AS \"Heure\",\n round((avg(Solaire) * count(Solaire) / 60.0 / 1000.0)::numeric, 2) AS \"Solaire\",\n round((avg(Reseau) * count(Reseau) / 60.0 / 1000.0)::numeric, 2) AS \"Reseau\",\n round((avg(Maison) * count(Maison) / 60.0 / 1000.0)::numeric, 2) AS \"Maison\",\n round((avg(Voiture) * count(Voiture) / 60.0 / 1000.0)::numeric, 2) AS \"Voiture\"\nFROM report, pas\nGROUP BY date_trunc(pas.unite, t), pas.forme\nHAVING count(Solaire) > 0 OR count(Reseau) > 0 OR count(Maison) > 0 OR count(Voiture) > 0\nORDER BY date_trunc(pas.unite, t)"
|
||||
"rawSql": "WITH bornes AS (\n -- ⚠️ LA MESURE NE PEUT PAS DEPASSER MAINTENANT. « La journee » s'etend\n -- desormais jusqu'a la fin du jour pour montrer la prevision ; sans ce\n -- garde-fou, le report de la derniere valeur connue (plus bas) remplirait les\n -- heures a venir avec la puissance de l'instant et gonflerait l'energie.\n SELECT $__timeFrom()::timestamptz AS d,\n least($__timeTo()::timestamptz, now()) AS f\n),\n-- Le pas s'adapte a la plage choisie. Sans cela, « 30 derniers jours » rend\n-- 720 barres illisibles — c'est exactement ce que fait VRM en passant de\n-- l'heure au jour puis au mois.\npas AS (\n SELECT CASE WHEN f - d > interval '70 days' THEN 'month'\n WHEN f - d > interval '4 days' THEN 'day'\n ELSE 'hour' END AS unite,\n CASE WHEN f - d > interval '70 days' THEN 'MM/YYYY'\n WHEN f - d > interval '4 days' THEN 'DD/MM'\n ELSE 'DD/MM HH24' END AS forme\n FROM bornes\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 Solaire,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_pv_power_system'))[1] AS d_Solaire,\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_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_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 FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npar_minute AS (\n SELECT axe.t, releve.Solaire, releve.d_Solaire, releve.Reseau, releve.d_Reseau, releve.Maison, releve.d_Maison, releve.Voiture, releve.d_Voiture\n FROM axe LEFT JOIN releve USING (t)\n),\npaliers AS (\n SELECT t, Solaire, d_Solaire, Reseau, d_Reseau, Maison, d_Maison, Voiture, d_Voiture,\n count(d_Solaire) OVER (ORDER BY t) AS g_Solaire,\n count(d_Reseau) OVER (ORDER BY t) AS g_Reseau,\n count(d_Maison) OVER (ORDER BY t) AS g_Maison,\n count(d_Voiture) OVER (ORDER BY t) AS g_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n coalesce(Solaire, first_value(d_Solaire) OVER (PARTITION BY g_Solaire ORDER BY t)) AS Solaire,\n coalesce(Reseau, first_value(d_Reseau) OVER (PARTITION BY g_Reseau ORDER BY t)) AS Reseau,\n coalesce(Maison, first_value(d_Maison) OVER (PARTITION BY g_Maison ORDER BY t)) AS Maison,\n coalesce(Voiture, first_value(d_Voiture) OVER (PARTITION BY g_Voiture ORDER BY t)) AS Voiture\n FROM paliers\n),\nmesure_par_periode AS (\n SELECT date_trunc(pas.unite, t) AS b,\n round((avg(Solaire) * count(Solaire) / 60.0 / 1000.0)::numeric, 2) AS \"Solaire\",\n round((avg(Reseau) * count(Reseau) / 60.0 / 1000.0)::numeric, 2) AS \"Reseau\",\n round((avg(Maison) * count(Maison) / 60.0 / 1000.0)::numeric, 2) AS \"Maison\",\n round((avg(Voiture) * count(Voiture) / 60.0 / 1000.0)::numeric, 2) AS \"Voiture\"\n FROM report, pas\n GROUP BY date_trunc(pas.unite, t)\n HAVING count(Solaire) > 0 OR count(Reseau) > 0 OR count(Maison) > 0 OR count(Voiture) > 0\n)\n\nSELECT to_char(b, (SELECT forme FROM pas)) AS \"Heure\",\n \"Solaire\",\n \"Reseau\",\n \"Maison\",\n \"Voiture\"\nFROM mesure_par_periode\nORDER BY b"
|
||||
}
|
||||
],
|
||||
"gridPos": {
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "Maintenant",
|
||||
"description": "Marge = ce que la batterie a, moins ce qu'il lui faut pour tenir jusqu'au relais du solaire. Negative, la nuit ne passe pas sans reseau.",
|
||||
"description": "Marge = ce que la batterie a, moins ce qu'il lui faut pour tenir jusqu'au relais du solaire. Negative, la nuit ne passe pas sans reseau. « Soleil restant » est le seul chiffre de ce bandeau qui ne soit pas une mesure : c'est la derniere prevision pour les heures qui restent aujourd'hui, l'heure en cours exclue.",
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "evcs-pg"
|
||||
@@ -77,7 +77,7 @@
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"editorMode": "code",
|
||||
"rawSql": "SELECT\n (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_battery_soc'\n ORDER BY ts DESC LIMIT 1) AS \"Batterie\",\n (SELECT soc_pct - soc_requis_pct FROM bilan\n WHERE soc_requis_pct IS NOT NULL ORDER BY ts DESC LIMIT 1) AS \"Marge\",\n (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_pv_power_system'\n ORDER BY ts DESC LIMIT 1)\n- (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_consumption_power_l1'\n ORDER BY ts DESC LIMIT 1) AS \"Excedent\""
|
||||
"rawSql": "SELECT\n (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_battery_soc'\n ORDER BY ts DESC LIMIT 1) AS \"Batterie\",\n (SELECT soc_pct - soc_requis_pct FROM bilan\n WHERE soc_requis_pct IS NOT NULL ORDER BY ts DESC LIMIT 1) AS \"Marge\",\n (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_pv_power_system'\n ORDER BY ts DESC LIMIT 1)\n- (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_consumption_power_l1'\n ORDER BY ts DESC LIMIT 1) AS \"Excedent\",\n (SELECT round(coalesce(sum(kwh), 0)::numeric, 1)\n FROM (\n SELECT (c.jour + (u.i - 1) * interval '1 hour') AT TIME ZONE 'Europe/Paris' AS heure,\n u.v AS kwh\n FROM (SELECT DISTINCT ON (jour) jour, horaire FROM prevision\n WHERE jour >= current_date ORDER BY jour, publie_a DESC) c,\n unnest(c.horaire) WITH ORDINALITY AS u(v, i)\n WHERE (c.jour + (u.i - 1) * interval '1 hour') AT TIME ZONE 'Europe/Paris'\n > date_trunc('hour', now())\n) p\n WHERE p.heure < date_trunc('day', now()) + interval '1 day')\n AS \"Soleil restant\""
|
||||
}
|
||||
],
|
||||
"gridPos": {
|
||||
@@ -202,6 +202,29 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Soleil restant"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "kwatth"
|
||||
},
|
||||
{
|
||||
"id": "decimals",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"mode": "fixed",
|
||||
"fixedColor": "#9e9e9e"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -226,7 +249,7 @@
|
||||
{
|
||||
"type": "stat",
|
||||
"title": "La nuit qui vient",
|
||||
"description": "Energie a couvrir d'ici l'heure ou la production repasse au-dessus de la consommation, et le niveau de batterie qu'il faudrait pour y arriver.",
|
||||
"description": "Energie a couvrir d'ici l'heure ou la production repasse au-dessus de la consommation, et le niveau de batterie qu'il faudrait pour y arriver. « Soleil demain » dit de combien la batterie pourra se refaire une fois la nuit passee : c'est lui qui decide jusqu'ou on peut la laisser descendre.",
|
||||
"datasource": {
|
||||
"type": "grafana-postgresql-datasource",
|
||||
"uid": "evcs-pg"
|
||||
@@ -241,7 +264,7 @@
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"editorMode": "code",
|
||||
"rawSql": "SELECT round(energie_a_couvrir_kwh::numeric, 1) AS \"A couvrir\",\n round(soc_requis_pct::numeric) AS \"SOC requis\",\n heure_croisement AS \"Relais du solaire\"\nFROM bilan ORDER BY ts DESC LIMIT 1"
|
||||
"rawSql": "SELECT round(energie_a_couvrir_kwh::numeric, 1) AS \"A couvrir\",\n round(soc_requis_pct::numeric) AS \"SOC requis\",\n heure_croisement AS \"Relais du solaire\",\n (SELECT round(coalesce(sum(kwh), 0)::numeric, 1)\n FROM (\n SELECT (c.jour + (u.i - 1) * interval '1 hour') AT TIME ZONE 'Europe/Paris' AS heure,\n u.v AS kwh\n FROM (SELECT DISTINCT ON (jour) jour, horaire FROM prevision\n WHERE jour >= current_date ORDER BY jour, publie_a DESC) c,\n unnest(c.horaire) WITH ORDINALITY AS u(v, i)\n WHERE (c.jour + (u.i - 1) * interval '1 hour') AT TIME ZONE 'Europe/Paris'\n > date_trunc('hour', now())\n) p\n WHERE p.heure >= date_trunc('day', now()) + interval '1 day')\n AS \"Soleil demain\"\nFROM bilan ORDER BY ts DESC LIMIT 1"
|
||||
}
|
||||
],
|
||||
"gridPos": {
|
||||
@@ -314,6 +337,29 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "Soleil demain"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "kwatth"
|
||||
},
|
||||
{
|
||||
"id": "decimals",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"mode": "fixed",
|
||||
"fixedColor": "#9e9e9e"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -130,7 +130,7 @@ def table(title, targets, ds, x, y, w, h, desc="", overrides=None):
|
||||
"options": {"showHeader": True, "cellHeight": "sm",
|
||||
"footer": {"show": False}}}
|
||||
|
||||
def barchart(title, targets, ds, x, y, w, h, unit="", desc=""):
|
||||
def barchart(title, targets, ds, x, y, w, h, unit="", desc="", overrides=None):
|
||||
return {"type": "barchart", "title": title, "description": desc,
|
||||
"datasource": ds, "targets": targets,
|
||||
"gridPos": {"x": x, "y": y, "w": w, "h": h},
|
||||
@@ -138,7 +138,7 @@ def barchart(title, targets, ds, x, y, w, h, unit="", desc=""):
|
||||
"color": {"mode": "palette-classic"},
|
||||
"custom": {"lineWidth": 1, "fillOpacity": 80,
|
||||
"gradientMode": "none", "axisBorderShow": False}},
|
||||
"overrides": []},
|
||||
"overrides": overrides or []},
|
||||
"options": {"orientation": "auto", "xTickLabelRotation": -45,
|
||||
"showValue": "never", "stacking": "none",
|
||||
"groupWidth": 0.7, "barWidth": 0.9,
|
||||
@@ -146,13 +146,18 @@ def barchart(title, targets, ds, x, y, w, h, unit="", desc=""):
|
||||
"showLegend": True},
|
||||
"tooltip": {"mode": "multi", "sort": "none"}}}
|
||||
|
||||
def dashboard(uid, title, desc, panels, refresh="1m", frm="now-24h", variables=None):
|
||||
def dashboard(uid, title, desc, panels, refresh="1m", frm="now-24h",
|
||||
jusqu="now", variables=None):
|
||||
return {
|
||||
"templating": {"list": variables or []},
|
||||
"uid": uid, "title": title, "description": desc,
|
||||
"tags": ["evcs"], "timezone": "Europe/Paris", "editable": True,
|
||||
"schemaVersion": 39, "version": 1, "refresh": refresh,
|
||||
"time": {"from": frm, "to": "now"},
|
||||
# `jusqu` depasse « maintenant » sur les tableaux qui montrent une
|
||||
# prevision : sans cela la partie a venir tombe hors du cadre, et le
|
||||
# tableau ne montre que du passe. Les requetes de MESURE bornent alors
|
||||
# elles-memes leur fin a `now()` — voir energie_sql().
|
||||
"time": {"from": frm, "to": jusqu},
|
||||
"timepicker": {"refresh_intervals": ["30s", "1m", "5m", "15m", "1h"]},
|
||||
"annotations": {"list": [
|
||||
{"builtIn": 1, "datasource": {"type": "grafana", "uid": "-- Grafana --"},
|
||||
@@ -179,6 +184,23 @@ def dashboard(uid, title, desc, panels, refresh="1m", frm="now-24h", variables=N
|
||||
"AND genre <> 'bilan' ORDER BY 1"}}]},
|
||||
"panels": panels}
|
||||
|
||||
# La prevision LA PLUS RECENTE, mise a plat en (heure locale, kWh), et bornee
|
||||
# aux heures STRICTEMENT a venir.
|
||||
#
|
||||
# ⚠️ L'heure EN COURS est volontairement exclue : elle est deja mesuree pour ses
|
||||
# minutes ecoulees. Y superposer une barre de prevision d'heure pleine ferait
|
||||
# compter deux fois la meme heure et donnerait un total faux.
|
||||
PREVISION_A_VENIR = """
|
||||
SELECT (c.jour + (u.i - 1) * interval '1 hour') AT TIME ZONE 'Europe/Paris' AS heure,
|
||||
u.v AS kwh
|
||||
FROM (SELECT DISTINCT ON (jour) jour, horaire FROM prevision
|
||||
WHERE jour >= current_date ORDER BY jour, publie_a DESC) c,
|
||||
unnest(c.horaire) WITH ORDINALITY AS u(v, i)
|
||||
WHERE (c.jour + (u.i - 1) * interval '1 hour') AT TIME ZONE 'Europe/Paris'
|
||||
> date_trunc('hour', now())
|
||||
"""
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 1. LA NUIT — la question a laquelle on ne savait pas repondre le 03/09
|
||||
# =====================================================================
|
||||
@@ -541,10 +563,17 @@ telephone = dashboard("evcs-telephone", "Sur le telephone", (
|
||||
" (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_pv_power_system'\n"
|
||||
" ORDER BY ts DESC LIMIT 1)\n"
|
||||
"- (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_consumption_power_l1'\n"
|
||||
" ORDER BY ts DESC LIMIT 1) AS \"Excedent\"", fmt="table")],
|
||||
" ORDER BY ts DESC LIMIT 1) AS \"Excedent\",\n"
|
||||
" (SELECT round(coalesce(sum(kwh), 0)::numeric, 1)\n"
|
||||
" FROM (" + PREVISION_A_VENIR + ") p\n"
|
||||
" WHERE p.heure < date_trunc('day', now()) + interval '1 day')\n"
|
||||
" AS \"Soleil restant\"", fmt="table")],
|
||||
PG, 0, 0, 24, 5,
|
||||
"Marge = ce que la batterie a, moins ce qu'il lui faut pour tenir jusqu'au "
|
||||
"relais du solaire. Negative, la nuit ne passe pas sans reseau.",
|
||||
"relais du solaire. Negative, la nuit ne passe pas sans reseau. "
|
||||
"« Soleil restant » est le seul chiffre de ce bandeau qui ne soit pas une "
|
||||
"mesure : c'est la derniere prevision pour les heures qui restent "
|
||||
"aujourd'hui, l'heure en cours exclue.",
|
||||
overrides=[
|
||||
sur("Batterie", [{"id": "unit", "value": "percent"},
|
||||
{"id": "thresholds", "value": seuils(
|
||||
@@ -555,22 +584,37 @@ telephone = dashboard("evcs-telephone", "Sur le telephone", (
|
||||
sur("Excedent", [{"id": "unit", "value": "watt"},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "thresholds", "value": seuils(
|
||||
("blue", None), ("green", 0))}])]),
|
||||
("blue", None), ("green", 0))}]),
|
||||
sur("Soleil restant", [{"id": "unit", "value": "kwatth"},
|
||||
{"id": "decimals", "value": 1},
|
||||
{"id": "color",
|
||||
"value": {"mode": "fixed", "fixedColor": "#9e9e9e"}}])]),
|
||||
|
||||
stat("La nuit qui vient", [
|
||||
sql("A",
|
||||
"SELECT round(energie_a_couvrir_kwh::numeric, 1) AS \"A couvrir\",\n"
|
||||
" round(soc_requis_pct::numeric) AS \"SOC requis\",\n"
|
||||
" heure_croisement AS \"Relais du solaire\"\n"
|
||||
" heure_croisement AS \"Relais du solaire\",\n"
|
||||
" (SELECT round(coalesce(sum(kwh), 0)::numeric, 1)\n"
|
||||
" FROM (" + PREVISION_A_VENIR + ") p\n"
|
||||
" WHERE p.heure >= date_trunc('day', now()) + interval '1 day')\n"
|
||||
" AS \"Soleil demain\"\n"
|
||||
"FROM bilan ORDER BY ts DESC LIMIT 1", fmt="table")],
|
||||
PG, 0, 5, 24, 5,
|
||||
"Energie a couvrir d'ici l'heure ou la production repasse au-dessus de la "
|
||||
"consommation, et le niveau de batterie qu'il faudrait pour y arriver.",
|
||||
"consommation, et le niveau de batterie qu'il faudrait pour y arriver. "
|
||||
"« Soleil demain » dit de combien la batterie pourra se refaire une fois "
|
||||
"la nuit passee : c'est lui qui decide jusqu'ou on peut la laisser "
|
||||
"descendre.",
|
||||
overrides=[
|
||||
sur("A couvrir", [{"id": "unit", "value": "kwatth"}]),
|
||||
sur("SOC requis", [{"id": "unit", "value": "percent"},
|
||||
{"id": "thresholds", "value": seuils(
|
||||
("green", None), ("yellow", 60), ("orange", 85), ("red", 100))}])]),
|
||||
("green", None), ("yellow", 60), ("orange", 85), ("red", 100))}]),
|
||||
sur("Soleil demain", [{"id": "unit", "value": "kwatth"},
|
||||
{"id": "decimals", "value": 1},
|
||||
{"id": "color",
|
||||
"value": {"mode": "fixed", "fixedColor": "#9e9e9e"}}])]),
|
||||
|
||||
ts("Ce qu'elle a, ce qu'il lui faut", [
|
||||
sql("A", "SELECT ts AS time,\n"
|
||||
@@ -691,8 +735,18 @@ ENERGIE_CLES = [
|
||||
# D'ou l'axe des minutes COMPLET par `generate_series`, la valeur reportee
|
||||
# depuis le dernier point connu, puis la moyenne. Chaque heure pese alors ses
|
||||
# soixante minutes, qu'il y ait eu une mesure ou non.
|
||||
ENERGIE_SQL = """WITH bornes AS (
|
||||
SELECT $__timeFrom()::timestamptz AS d, $__timeTo()::timestamptz AS f
|
||||
|
||||
|
||||
def energie_sql(prevision=False):
|
||||
"""Energie par periode. Avec `prevision`, une colonne « Encore attendu »
|
||||
prolonge le graphique sur les heures a venir."""
|
||||
return """WITH bornes AS (
|
||||
-- ⚠️ LA MESURE NE PEUT PAS DEPASSER MAINTENANT. « La journee » s'etend
|
||||
-- desormais jusqu'a la fin du jour pour montrer la prevision ; sans ce
|
||||
-- garde-fou, le report de la derniere valeur connue (plus bas) remplirait les
|
||||
-- heures a venir avec la puissance de l'instant et gonflerait l'energie.
|
||||
SELECT $__timeFrom()::timestamptz AS d,
|
||||
least($__timeTo()::timestamptz, now()) AS f
|
||||
),
|
||||
-- Le pas s'adapte a la plage choisie. Sans cela, « 30 derniers jours » rend
|
||||
-- 720 barres illisibles — c'est exactement ce que fait VRM en passant de
|
||||
@@ -736,16 +790,43 @@ report AS (
|
||||
f" coalesce({n}, first_value(d_{n}) OVER (PARTITION BY g_{n} ORDER BY t)) AS {n}"
|
||||
for _, n in ENERGIE_CLES) + """
|
||||
FROM paliers
|
||||
)
|
||||
SELECT to_char(date_trunc(pas.unite, t), pas.forme) AS "Heure",
|
||||
),
|
||||
mesure_par_periode AS (
|
||||
SELECT date_trunc(pas.unite, t) AS b,
|
||||
""" + ",\n".join(
|
||||
# Energie REELLEMENT accumulee : puissance moyenne x duree observee.
|
||||
f' round((avg({n}) * count({n}) / 60.0 / 1000.0)::numeric, 2) AS "{n}"'
|
||||
f' round((avg({n}) * count({n}) / 60.0 / 1000.0)::numeric, 2) AS "{n}"'
|
||||
for _, n in ENERGIE_CLES) + """
|
||||
FROM report, pas
|
||||
GROUP BY date_trunc(pas.unite, t), pas.forme
|
||||
HAVING """ + " OR ".join(f"count({n}) > 0" for _, n in ENERGIE_CLES) + """
|
||||
ORDER BY date_trunc(pas.unite, t)"""
|
||||
FROM report, pas
|
||||
GROUP BY date_trunc(pas.unite, t)
|
||||
HAVING """ + " OR ".join(f"count({n}) > 0" for _, n in ENERGIE_CLES) + """
|
||||
)
|
||||
""" + ("""
|
||||
, a_venir AS (
|
||||
SELECT date_trunc((SELECT unite FROM pas), p.heure) AS b,
|
||||
sum(p.kwh) AS kwh
|
||||
FROM (""" + PREVISION_A_VENIR + """) p
|
||||
WHERE p.heure >= (SELECT d FROM bornes)
|
||||
-- Borne STRICTE : une periode qui commence pile a la fin de la fenetre
|
||||
-- appartient a la suivante. Sans cela, « La journee » affiche une barre
|
||||
-- de demain 00 h.
|
||||
AND p.heure < $__timeTo()::timestamptz
|
||||
GROUP BY 1
|
||||
)
|
||||
-- FULL OUTER : une periode a venir n'a AUCUNE ligne de mesure, une periode
|
||||
-- passee n'a aucune prevision. Ni l'une ni l'autre ne doit disparaitre.
|
||||
SELECT to_char(coalesce(m.b, a.b), (SELECT forme FROM pas)) AS "Heure",
|
||||
""" + ",\n".join(f' m."{n}"' for _, n in ENERGIE_CLES) + """,
|
||||
round(a.kwh::numeric, 2) AS "Encore attendu"
|
||||
FROM mesure_par_periode m FULL OUTER JOIN a_venir a ON a.b = m.b
|
||||
ORDER BY coalesce(m.b, a.b)""" if prevision else """
|
||||
SELECT to_char(b, (SELECT forme FROM pas)) AS "Heure",
|
||||
""" + ",\n".join(f' "{n}"' for _, n in ENERGIE_CLES) + """
|
||||
FROM mesure_par_periode
|
||||
ORDER BY b""")
|
||||
|
||||
|
||||
ENERGIE_SQL = energie_sql()
|
||||
|
||||
# Detail par tracker. Les quatre entrees du RS 450/200 et le MPPT1 ne suivent
|
||||
# pas la meme convention de nommage : deux branches, reunies puis pivotees.
|
||||
@@ -1296,7 +1377,12 @@ BILAN_TOTAUX = BILAN_BASE + """SELECT
|
||||
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"
|
||||
/ nullif(sum(maison), 0))::numeric, 0) AS "Autonomie",
|
||||
-- Ce qui reste a venir aujourd'hui, a cote de ce qui est deja fait. Sous-
|
||||
-- requete independante de `report` : elle ne touche a aucun total mesure.
|
||||
(SELECT round(coalesce(sum(kwh), 0)::numeric, 1)
|
||||
FROM (""" + PREVISION_A_VENIR + """) p
|
||||
WHERE p.heure < date_trunc('day', now()) + interval '1 day') AS "Reste attendu"
|
||||
FROM report GROUP BY (1 = 1)"""
|
||||
|
||||
BILAN_FLUX = BILAN_BASE + """SELECT t AS time,
|
||||
@@ -1318,7 +1404,9 @@ journee = dashboard(
|
||||
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.",
|
||||
"differe et non une consommation supplementaire. « Reste attendu » "
|
||||
"est la seule colonne qui ne soit pas une mesure : c'est ce que la "
|
||||
"derniere prevision annonce pour les heures qui restent.",
|
||||
overrides=[sur(n, [{"id": "unit", "value": "kwatth"},
|
||||
{"id": "decimals", "value": 1}]) for n in
|
||||
("Solaire produit", "Maison consommee", "Voiture",
|
||||
@@ -1330,6 +1418,13 @@ journee = dashboard(
|
||||
{"id": "thresholds", "value": seuils(
|
||||
("green", None), ("#EAB839", 3),
|
||||
("orange", 8))}]),
|
||||
sur("Reste attendu", [{"id": "unit", "value": "kwatth"},
|
||||
{"id": "decimals", "value": 1},
|
||||
{"id": "custom.cellOptions",
|
||||
"value": {"type": "color-text"}},
|
||||
{"id": "color",
|
||||
"value": {"mode": "fixed",
|
||||
"fixedColor": "#9e9e9e"}}]),
|
||||
sur("Autonomie", [{"id": "unit", "value": "percent"},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "custom.cellOptions",
|
||||
@@ -1342,10 +1437,19 @@ journee = dashboard(
|
||||
# nommees par la variable $vue de « Le systeme ». Sur un tableau qui n'a pas
|
||||
# cette variable, elle filtre TOUT et Grafana affiche « No numeric fields
|
||||
# found ». Ici on veut les quatre colonnes, tout le temps.
|
||||
barchart("Energie heure par heure", [sql("A", ENERGIE_SQL, fmt="table")],
|
||||
barchart("Energie heure par heure",
|
||||
[sql("A", energie_sql(prevision=True), fmt="table")],
|
||||
PG, 0, 5, 24, 9, "kwatth",
|
||||
"La forme de la journee : quand le solaire a couvert la maison, et quand "
|
||||
"il a fallu acheter. Le pas suit la periode choisie."),
|
||||
"La forme de la journee : quand le solaire a couvert la maison, quand il "
|
||||
"a fallu acheter — et, a la suite des barres mesurees, ce que la derniere "
|
||||
"prevision annonce pour les heures qui restent. L'heure EN COURS n'a pas "
|
||||
"de barre de prevision : elle est deja mesuree pour ses minutes ecoulees.",
|
||||
overrides=[
|
||||
# Gris clair et translucide : ce n'est pas une mesure, et le coup d'oeil
|
||||
# doit le dire avant la legende.
|
||||
sur("Encore attendu", [
|
||||
{"id": "color", "value": {"mode": "fixed", "fixedColor": "#9e9e9e"}},
|
||||
{"id": "custom.fillOpacity", "value": 35}])]),
|
||||
|
||||
ts("Les flux de la journee", [sql("A", BILAN_FLUX)],
|
||||
PG, 0, 14, 24, 10, "watt",
|
||||
@@ -1405,7 +1509,7 @@ journee = dashboard(
|
||||
desc="Le genre `bilan` est ecarte : ses bascules de production bridee "
|
||||
"changent plusieurs fois par heure et noieraient le reste. Elles "
|
||||
"sont lisibles dans la frise ci-dessus."),
|
||||
], frm="now/d")
|
||||
], frm="now/d", jusqu="now/d")
|
||||
|
||||
|
||||
base = pathlib.Path('deploy/historisation/grafana/dashboards')
|
||||
|
||||
Reference in New Issue
Block a user