diff --git a/HANDOFF.md b/HANDOFF.md index 29e6744..118bd1b 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -5479,3 +5479,60 @@ inspecter. --- *Document mis a jour le : 07/09/2026 — Fix H prepare (golden 84/84, gabarit valide contre HA), en attente de validation avant PUT* + + +## 60. Session du 07/09/2026 — Tableau « La journee », et un report de valeur qui fabriquait de l'energie + +### Le tableau +`evcs-journee` (« La journee »), fenetre **`now/d`** — le jour civil en cours et +non `now-24h`, qui melangerait deux journees et rendrait le total de production +incomparable d'un jour a l'autre. Sept panneaux : le bilan depuis minuit +(production, maison, reseau, voiture, batterie chargee/rendue, autonomie), +l'energie heure par heure, les cinq flux sur un axe commun, le SOC contre le +SOC requis par le bilan, une frise source/decisions/censure, les charges du jour +et le journal des evenements. + +L'autonomie se calcule sur la maison, pas sur la maison + voiture : sinon une +grosse charge solaire du vehicule gonfle artificiellement le taux. + +### ⚠️ Le defaut trouve en controlant les chiffres — il touchait TOUS les tableaux +Premiere version du bilan : **6,9 kWh pour la voiture**, alors que le compteur +cumulatif de la borne et la table `session_charge` donnent tous deux **3,6 kWh**. + +Cause, identifiee au watt pres. Le report de la derniere valeur connue +propageait la **MOYENNE de la minute** au lieu de sa **derniere valeur**. Le +07/09 a 14:28, la borne passe de 1577 W a 0 W dans la meme minute : cette minute +vaut donc 788 W en moyenne, et cette moyenne etait ensuite maintenue pendant +toutes les heures sans ecriture qui suivaient. 788 W x 60 min = **0,79 kWh +fabriques sur la seule heure de 15 h**, et le meme mecanisme sur 19 h et 20 h. + +Le defaut ne se voyait ni sur le solaire ni sur la maison — ecrits en continu, +donc jamais de long report. **Il frappe les grandeurs intermittentes**, au +premier rang desquelles la borne, qui passe l'essentiel de la journee a zero. + +Correctif applique aux **trois** constructeurs de requetes +(`sql_grandeurs`, `ENERGIE_SQL`, `BILAN_BASE`) : la minute observee garde sa +moyenne — juste pour l'energie consommee DANS cette minute — mais le report +vers les minutes suivantes propage +`(array_agg(valeur ORDER BY ts DESC))[1]`, c'est-a-dire la derniere valeur +reellement lue. Aucun seuil, aucune peremption arbitraire. + +Verification : apres correction, « Voiture » tombe a **3,6 kWh**, egal au +centieme a la somme des sessions mesurees au compteur de la borne. Les autres +totaux sont inchanges (22,1 / 26,1 / 6,2), ce qui confirme que seul +l'intermittent etait touche. 73/73 requetes valides. + +⚠️ **Consequence a garder en tete** : les colonnes « Voiture » des tableaux +« Le systeme » et « Les vehicules » etaient fausses depuis leur creation +(04/09), dans le sens de la SURESTIMATION. Toute lecture d'energie vehicule +anterieure au 07/09 doit etre reprise. + +### Rappel de methode, deuxieme fois en trois jours +`verifier-dashboards.mjs` prouve qu'une requete s'EXECUTE, pas qu'elle rend la +grandeur voulue — il annoncait 73/73 avec le chiffre faux. Le seul controle qui +attrape ce genre de defaut est de confronter le resultat a une source +independante : ici le compteur cumulatif de la borne et la table des sessions. +Meme lecon que le 04/09 (colonne « Excedent » integralement nulle, 19/19 vertes). + +--- +*Document mis a jour le : 07/09/2026 — tableau « La journee », report de la derniere valeur (et non de la moyenne) dans les trois constructeurs de requetes* diff --git a/deploy/historisation/grafana/dashboards/journee.json b/deploy/historisation/grafana/dashboards/journee.json new file mode 100644 index 0000000..c07d5ee --- /dev/null +++ b/deploy/historisation/grafana/dashboards/journee.json @@ -0,0 +1,580 @@ +{ + "templating": { + "list": [] + }, + "uid": "evcs-journee", + "title": "La journee", + "description": "Le bilan de la journee en cours, de minuit a maintenant : ce qui a ete produit, ce qui a ete consomme, ce qui a ete achete, et ou c'est alle. Les energies sont integrees depuis les puissances mesurees.", + "tags": [ + "evcs" + ], + "timezone": "Europe/Paris", + "editable": true, + "schemaVersion": 39, + "version": 1, + "refresh": "1m", + "time": { + "from": "now/d", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "30s", + "1m", + "5m", + "15m", + "1h" + ] + }, + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "name": "Annotations & Alerts", + "iconColor": "rgba(0, 211, 255, 1)", + "type": "dashboard" + }, + { + "name": "Evenements", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "enable": false, + "iconColor": "semi-dark-orange", + "hide": false, + "target": { + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "SELECT ts AS time, resume AS text, genre || ',' || coalesce(source,'?') AS tags FROM evenement WHERE $__timeFilter(ts) AND genre <> 'bilan' ORDER BY 1" + } + } + ] + }, + "panels": [ + { + "type": "stat", + "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.", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "WITH axe AS (\n SELECT generate_series(date_trunc('minute', $__timeFrom()::timestamptz),\n date_trunc('minute', $__timeTo()::timestamptz),\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" + } + ], + "gridPos": { + "x": 0, + "y": 0, + "w": 24, + "h": 5 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [ + { + "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 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Reseau importe" + }, + "properties": [ + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "#EAB839", + "value": 3 + }, + { + "color": "orange", + "value": 8 + } + ] + } + } + ] + } + ] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/.*/", + "values": false + }, + "orientation": "horizontal", + "textMode": "auto", + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "percentChangeColorMode": "standard", + "noValue": "—", + "wideLayout": true + } + }, + { + "type": "barchart", + "title": "Energie heure par heure", + "description": "La forme de la journee : quand le solaire a couvert la maison, et quand il a fallu acheter. Le pas suit la periode choisie.", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "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)" + } + ], + "gridPos": { + "x": 0, + "y": 5, + "w": 24, + "h": 9 + }, + "fieldConfig": { + "defaults": { + "unit": "kwatth", + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 1, + "fillOpacity": 80, + "gradientMode": "none", + "axisBorderShow": false + } + }, + "overrides": [] + }, + "options": { + "orientation": "auto", + "xTickLabelRotation": -45, + "showValue": "never", + "stacking": "none", + "groupWidth": 0.7, + "barWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "pattern": "^(Time|time|Heure|$vue)$" + } + } + } + ] + }, + { + "type": "timeseries", + "title": "Les flux de la journee", + "description": "Les cinq puissances sur le meme axe. La batterie est signee : au-dessus de zero elle se remplit, en dessous elle porte la maison. Quand « Reseau » decolle alors que la production est forte, c'est une fermeture du contacteur qu'il faut expliquer.", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "time_series", + "rawQuery": true, + "editorMode": "code", + "rawSql": "WITH axe AS (\n SELECT generate_series(date_trunc('minute', $__timeFrom()::timestamptz),\n date_trunc('minute', $__timeTo()::timestamptz),\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 t AS time,\n pv AS \"Production solaire\",\n maison AS \"Consommation maison\",\n voiture AS \"Charge du vehicule\",\n reseau AS \"Reseau\",\n batterie AS \"Batterie (+ charge, - decharge)\"\nFROM report ORDER BY t" + } + ], + "gridPos": { + "x": 0, + "y": 14, + "w": 24, + "h": 10 + }, + "fieldConfig": { + "defaults": { + "unit": "watt", + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 8, + "showPoints": "never", + "spanNulls": true, + "axisBorderShow": false, + "gradientMode": "opacity" + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "type": "timeseries", + "title": "Etat de charge, et ce que la nuit exigera", + "description": "Le SOC reel contre ce que le bilan energetique reclame pour traverser la nuit jusqu'au croisement production/consommation. Quand le reel passe sous la ligne en fin de journee, le reseau sera necessaire avant le matin.", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "time_series", + "rawQuery": true, + "editorMode": "code", + "rawSql": "SELECT ts AS time, valeur AS \"SOC reel\"\nFROM mesure WHERE cle = 'sensor.victron_battery_soc'\n AND $__timeFilter(ts) ORDER BY 1" + }, + { + "refId": "B", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "time_series", + "rawQuery": true, + "editorMode": "code", + "rawSql": "SELECT ts AS time, soc_requis_pct AS \"SOC requis par le bilan\"\nFROM bilan WHERE $__timeFilter(ts) AND statut = 'ok' ORDER BY 1" + } + ], + "gridPos": { + "x": 0, + "y": 24, + "w": 12, + "h": 9 + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 8, + "showPoints": "never", + "spanNulls": true, + "axisBorderShow": false, + "gradientMode": "opacity" + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "type": "state-timeline", + "title": "Reseau, source et decisions", + "description": "Trois bandes qui se lisent ensemble. « Production bridee » signale que le solaire n'avait plus de preneur : l'energie manquante ce moment-la a ete refusee, pas absente.", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "SELECT ts AS time, source_mode AS \"Source\"\nFROM decision WHERE $__timeFilter(ts) ORDER BY 1" + }, + { + "refId": "B", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "SELECT ts AS time, etat AS \"Etat du controleur\"\nFROM decision WHERE $__timeFilter(ts) ORDER BY 1" + }, + { + "refId": "C", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "SELECT ts AS time,\n CASE WHEN pv_censure THEN 'production bridee'\n ELSE 'production libre' END AS \"Production\"\nFROM bilan WHERE $__timeFilter(ts) ORDER BY 1" + } + ], + "gridPos": { + "x": 12, + "y": 24, + "w": 12, + "h": 9 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "lineWidth": 0, + "fillOpacity": 80, + "insertNulls": false, + "spanNulls": true + }, + "mappings": [] + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single" + } + } + }, + { + "type": "table", + "title": "Les charges du jour", + "description": "⚠️ L'etiquette du vehicule vient d'un selecteur manuel : elle est fausse s'il n'a pas ete positionne au branchement. Les deux voitures sont electriquement indiscernables (3 680 W contre 3 655 W a consigne egale).", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "SELECT to_char(debut AT TIME ZONE 'Europe/Paris', 'HH24:MI') AS \"Debut\",\n coalesce(to_char(fin AT TIME ZONE 'Europe/Paris', 'HH24:MI'), 'en cours') AS \"Fin\",\n coalesce(vehicule, '?') AS \"Vehicule\",\n round(coalesce(energie_compteur_kwh, energie_kwh)::numeric, 2) AS \"Energie\",\n round(coalesce(p_max_w, 0)::numeric) AS \"Pointe\",\n coalesce(source_mode, '?') AS \"Source\"\nFROM session_charge WHERE $__timeFilter(debut) ORDER BY debut DESC" + } + ], + "gridPos": { + "x": 0, + "y": 33, + "w": 12, + "h": 8 + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "left", + "cellOptions": { + "type": "auto" + }, + "filterable": true + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Energie" + }, + "properties": [ + { + "id": "unit", + "value": "kwatth" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Pointe" + }, + "properties": [ + { + "id": "unit", + "value": "watt" + } + ] + } + ] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + }, + { + "type": "table", + "title": "Ce qui est arrive aujourd'hui", + "description": "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.", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "SELECT to_char(ts AT TIME ZONE 'Europe/Paris', 'HH24:MI:SS') AS \"Heure\",\n genre AS \"Genre\", resume AS \"Evenement\"\nFROM evenement WHERE $__timeFilter(ts) AND genre <> 'bilan'\nORDER BY ts DESC" + } + ], + "gridPos": { + "x": 12, + "y": 33, + "w": 12, + "h": 8 + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "left", + "cellOptions": { + "type": "auto" + }, + "filterable": true + } + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + } + ] +} diff --git a/deploy/historisation/grafana/dashboards/nuit.json b/deploy/historisation/grafana/dashboards/nuit.json index f0b0a15..db08dd8 100644 --- a/deploy/historisation/grafana/dashboards/nuit.json +++ b/deploy/historisation/grafana/dashboards/nuit.json @@ -359,7 +359,7 @@ "format": "time_series", "rawQuery": true, "editorMode": "code", - "rawSql": "WITH par_minute AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS Reseau,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_battery_power') AS Batterie,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS Maison,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS Voiture\n FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npaliers AS (\n SELECT t, Reseau, Batterie, Maison, Voiture,\n count(Reseau) OVER (ORDER BY t) AS g_Reseau,\n count(Batterie) OVER (ORDER BY t) AS g_Batterie,\n count(Maison) OVER (ORDER BY t) AS g_Maison,\n count(Voiture) OVER (ORDER BY t) AS g_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n first_value(Reseau) OVER (PARTITION BY g_Reseau ORDER BY t) AS Reseau,\n first_value(Batterie) OVER (PARTITION BY g_Batterie ORDER BY t) AS Batterie,\n first_value(Maison) OVER (PARTITION BY g_Maison ORDER BY t) AS Maison,\n first_value(Voiture) OVER (PARTITION BY g_Voiture ORDER BY t) AS Voiture\n FROM paliers\n)\nSELECT t AS time,\n Reseau AS \"Reseau\",\n Batterie AS \"Batterie\",\n Maison AS \"Maison\",\n Voiture AS \"Voiture\"\nFROM report ORDER BY t" + "rawSql": "WITH par_minute AS (\n SELECT date_trunc('minute', ts) AS t,\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_battery_power') AS Batterie,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_battery_power'))[1] AS d_Batterie,\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),\npaliers AS (\n SELECT t,\n Reseau, count(d_Reseau) OVER (ORDER BY t) AS g_Reseau,\n Batterie, count(d_Batterie) OVER (ORDER BY t) AS g_Batterie,\n Maison, count(d_Maison) OVER (ORDER BY t) AS g_Maison,\n Voiture, count(d_Voiture) OVER (ORDER BY t) AS g_Voiture,\n d_Reseau,\n d_Batterie,\n d_Maison,\n d_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n coalesce(Reseau, first_value(d_Reseau) OVER (PARTITION BY g_Reseau ORDER BY t)) AS Reseau,\n coalesce(Batterie, first_value(d_Batterie) OVER (PARTITION BY g_Batterie ORDER BY t)) AS Batterie,\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 t AS time,\n Reseau AS \"Reseau\",\n Batterie AS \"Batterie\",\n Maison AS \"Maison\",\n Voiture AS \"Voiture\"\nFROM report ORDER BY t" } ], "gridPos": { diff --git a/deploy/historisation/grafana/dashboards/solaire.json b/deploy/historisation/grafana/dashboards/solaire.json index dd76758..96e366d 100644 --- a/deploy/historisation/grafana/dashboards/solaire.json +++ b/deploy/historisation/grafana/dashboards/solaire.json @@ -77,7 +77,7 @@ "format": "time_series", "rawQuery": true, "editorMode": "code", - "rawSql": "WITH par_minute AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS Production,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS Maison,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS Voiture\n FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npaliers AS (\n SELECT t, Production, Maison, Voiture,\n count(Production) OVER (ORDER BY t) AS g_Production,\n count(Maison) OVER (ORDER BY t) AS g_Maison,\n count(Voiture) OVER (ORDER BY t) AS g_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n first_value(Production) OVER (PARTITION BY g_Production ORDER BY t) AS Production,\n first_value(Maison) OVER (PARTITION BY g_Maison ORDER BY t) AS Maison,\n first_value(Voiture) OVER (PARTITION BY g_Voiture ORDER BY t) AS Voiture\n FROM paliers\n)\nSELECT t AS time,\n Production AS \"Production\",\n Maison AS \"Maison\",\n Voiture AS \"Voiture\",\n Production - Maison AS \"Excedent\"\nFROM report ORDER BY t" + "rawSql": "WITH par_minute AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS Production,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_pv_power_system'))[1] AS d_Production,\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),\npaliers AS (\n SELECT t,\n Production, count(d_Production) OVER (ORDER BY t) AS g_Production,\n Maison, count(d_Maison) OVER (ORDER BY t) AS g_Maison,\n Voiture, count(d_Voiture) OVER (ORDER BY t) AS g_Voiture,\n d_Production,\n d_Maison,\n d_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n coalesce(Production, first_value(d_Production) OVER (PARTITION BY g_Production ORDER BY t)) AS Production,\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 t AS time,\n Production AS \"Production\",\n Maison AS \"Maison\",\n Voiture AS \"Voiture\",\n Production - Maison AS \"Excedent\"\nFROM report ORDER BY t" } ], "gridPos": { diff --git a/deploy/historisation/grafana/dashboards/systeme.json b/deploy/historisation/grafana/dashboards/systeme.json index aed90fd..c82fdbb 100644 --- a/deploy/historisation/grafana/dashboards/systeme.json +++ b/deploy/historisation/grafana/dashboards/systeme.json @@ -1098,7 +1098,7 @@ "format": "table", "rawQuery": true, "editorMode": "code", - "rawSql": "WITH par_minute AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS Solaire,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS Reseau,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_battery_power') AS Batterie,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS Maison,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS Voiture\n FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npaliers AS (\n SELECT t, Solaire, Reseau, Batterie, Maison, Voiture,\n count(Solaire) OVER (ORDER BY t) AS g_Solaire,\n count(Reseau) OVER (ORDER BY t) AS g_Reseau,\n count(Batterie) OVER (ORDER BY t) AS g_Batterie,\n count(Maison) OVER (ORDER BY t) AS g_Maison,\n count(Voiture) OVER (ORDER BY t) AS g_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n first_value(Solaire) OVER (PARTITION BY g_Solaire ORDER BY t) AS Solaire,\n first_value(Reseau) OVER (PARTITION BY g_Reseau ORDER BY t) AS Reseau,\n first_value(Batterie) OVER (PARTITION BY g_Batterie ORDER BY t) AS Batterie,\n first_value(Maison) OVER (PARTITION BY g_Maison ORDER BY t) AS Maison,\n first_value(Voiture) OVER (PARTITION BY g_Voiture ORDER BY t) AS Voiture\n FROM paliers\n)\nSELECT t AS time,\n Solaire AS \"Solaire\",\n Reseau AS \"Reseau\",\n Batterie AS \"Batterie\",\n Maison AS \"Maison\",\n Voiture AS \"Voiture\"\nFROM report ORDER BY t" + "rawSql": "WITH par_minute 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_battery_power') AS Batterie,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_battery_power'))[1] AS d_Batterie,\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),\npaliers AS (\n SELECT t,\n Solaire, count(d_Solaire) OVER (ORDER BY t) AS g_Solaire,\n Reseau, count(d_Reseau) OVER (ORDER BY t) AS g_Reseau,\n Batterie, count(d_Batterie) OVER (ORDER BY t) AS g_Batterie,\n Maison, count(d_Maison) OVER (ORDER BY t) AS g_Maison,\n Voiture, count(d_Voiture) OVER (ORDER BY t) AS g_Voiture,\n d_Solaire,\n d_Reseau,\n d_Batterie,\n d_Maison,\n d_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(Batterie, first_value(d_Batterie) OVER (PARTITION BY g_Batterie ORDER BY t)) AS Batterie,\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 t AS time,\n Solaire AS \"Solaire\",\n Reseau AS \"Reseau\",\n Batterie AS \"Batterie\",\n Maison AS \"Maison\",\n Voiture AS \"Voiture\"\nFROM report ORDER BY t" } ], "gridPos": { @@ -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 avg(valeur) FILTER (WHERE cle = 'sensor.victron_grid_power_l1') AS Reseau,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS Maison,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_evcs_ac_power') AS Voiture\n FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npar_minute AS (\n SELECT axe.t, releve.Solaire, releve.Reseau, releve.Maison, releve.Voiture\n FROM axe LEFT JOIN releve USING (t)\n),\npaliers AS (\n SELECT t, Solaire, Reseau, Maison, Voiture,\n count(Solaire) OVER (ORDER BY t) AS g_Solaire,\n count(Reseau) OVER (ORDER BY t) AS g_Reseau,\n count(Maison) OVER (ORDER BY t) AS g_Maison,\n count(Voiture) OVER (ORDER BY t) AS g_Voiture\n FROM par_minute\n),\nreport AS (\n SELECT t,\n first_value(Solaire) OVER (PARTITION BY g_Solaire ORDER BY t) AS Solaire,\n first_value(Reseau) OVER (PARTITION BY g_Reseau ORDER BY t) AS Reseau,\n first_value(Maison) OVER (PARTITION BY g_Maison ORDER BY t) AS Maison,\n first_value(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 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)" } ], "gridPos": { diff --git a/deploy/historisation/grafana/dashboards/telephone.json b/deploy/historisation/grafana/dashboards/telephone.json index 63d6190..0f3e6c4 100644 --- a/deploy/historisation/grafana/dashboards/telephone.json +++ b/deploy/historisation/grafana/dashboards/telephone.json @@ -451,7 +451,7 @@ "format": "time_series", "rawQuery": true, "editorMode": "code", - "rawSql": "WITH par_minute AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS Production,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS Maison\n FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npaliers AS (\n SELECT t, Production, Maison,\n count(Production) OVER (ORDER BY t) AS g_Production,\n count(Maison) OVER (ORDER BY t) AS g_Maison\n FROM par_minute\n),\nreport AS (\n SELECT t,\n first_value(Production) OVER (PARTITION BY g_Production ORDER BY t) AS Production,\n first_value(Maison) OVER (PARTITION BY g_Maison ORDER BY t) AS Maison\n FROM paliers\n)\nSELECT t AS time,\n Production AS \"Production\",\n Maison AS \"Maison\",\n Production - Maison AS \"Excedent\"\nFROM report ORDER BY t" + "rawSql": "WITH par_minute AS (\n SELECT date_trunc('minute', ts) AS t,\n avg(valeur) FILTER (WHERE cle = 'sensor.victron_pv_power_system') AS Production,\n (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = 'sensor.victron_pv_power_system'))[1] AS d_Production,\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 FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n),\npaliers AS (\n SELECT t,\n Production, count(d_Production) OVER (ORDER BY t) AS g_Production,\n Maison, count(d_Maison) OVER (ORDER BY t) AS g_Maison,\n d_Production,\n d_Maison\n FROM par_minute\n),\nreport AS (\n SELECT t,\n coalesce(Production, first_value(d_Production) OVER (PARTITION BY g_Production ORDER BY t)) AS Production,\n coalesce(Maison, first_value(d_Maison) OVER (PARTITION BY g_Maison ORDER BY t)) AS Maison\n FROM paliers\n)\nSELECT t AS time,\n Production AS \"Production\",\n Maison AS \"Maison\",\n Production - Maison AS \"Excedent\"\nFROM report ORDER BY t" } ], "gridPos": { diff --git a/tools/gen-grafana-dashboards.py b/tools/gen-grafana-dashboards.py index 3320707..4595a8a 100644 --- a/tools/gen-grafana-dashboards.py +++ b/tools/gen-grafana-dashboards.py @@ -34,17 +34,22 @@ def sql_grandeurs(refid, colonnes, calculees=None, fmt="time_series"): """ noms = [n for _, n in colonnes] par_minute = ",\n".join( - f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n}" for c, n in colonnes) - groupes = ",\n".join(f" count({n}) OVER (ORDER BY t) AS g_{n}" for n in noms) + f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n},\n" + f" (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = '{c}'))[1]" + f" AS d_{n}" for c, n in colonnes) + groupes = ",\n".join( + f" {n}, count(d_{n}) OVER (ORDER BY t) AS g_{n}" for n in noms) remplis = ",\n".join( - f" first_value({n}) OVER (PARTITION BY g_{n} ORDER BY t) AS {n}" for n in noms) + f" coalesce({n}, first_value(d_{n}) OVER (PARTITION BY g_{n} ORDER BY t))" + f" AS {n}" for n in noms) sorties = ",\n".join(f' {n} AS "{n.replace("_", " ")}"' for n in noms) if calculees: sorties += ",\n" + ",\n".join(f' {e} AS "{lib}"' for e, lib in calculees) q = (f"WITH par_minute AS (\n" f" SELECT date_trunc('minute', ts) AS t,\n{par_minute}\n" f" FROM mesure WHERE $__timeFilter(ts) GROUP BY 1\n" - f"),\npaliers AS (\n SELECT t, {', '.join(noms)},\n{groupes}\n FROM par_minute\n" + f"),\npaliers AS (\n SELECT t,\n{groupes},\n" + + ",\n".join(f" d_{n}" for n in noms) + f"\n FROM par_minute\n" f"),\nreport AS (\n SELECT t,\n{remplis}\n FROM paliers\n)\n" f"SELECT t AS time,\n{sorties}\nFROM report ORDER BY t") return sql(refid, q, fmt=fmt) @@ -709,22 +714,26 @@ axe AS ( releve AS ( SELECT date_trunc('minute', ts) AS t, """ + ",\n".join( - f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n}" for c, n in ENERGIE_CLES) + """ + f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n},\n" + f" (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = '{c}'))[1] AS d_{n}" + for c, n in ENERGIE_CLES) + """ FROM mesure WHERE $__timeFilter(ts) GROUP BY 1 ), par_minute AS ( - SELECT axe.t, """ + ", ".join(f"releve.{n}" for _, n in ENERGIE_CLES) + """ + SELECT axe.t, """ + ", ".join( + f"releve.{n}, releve.d_{n}" for _, n in ENERGIE_CLES) + """ FROM axe LEFT JOIN releve USING (t) ), paliers AS ( - SELECT t, """ + ", ".join(n for _, n in ENERGIE_CLES) + ",\n" + ",\n".join( - f" count({n}) OVER (ORDER BY t) AS g_{n}" for _, n in ENERGIE_CLES) + """ + SELECT t, """ + ", ".join( + f"{n}, d_{n}" for _, n in ENERGIE_CLES) + ",\n" + ",\n".join( + f" count(d_{n}) OVER (ORDER BY t) AS g_{n}" for _, n in ENERGIE_CLES) + """ FROM par_minute ), report AS ( SELECT t, """ + ",\n".join( - f" first_value({n}) OVER (PARTITION BY g_{n} ORDER BY t) AS {n}" + f" coalesce({n}, first_value(d_{n}) OVER (PARTITION BY g_{n} ORDER BY t)) AS {n}" for _, n in ENERGIE_CLES) + """ FROM paliers ) @@ -1197,8 +1206,173 @@ batteries = dashboard("evcs-batteries", "Les batteries", ( ], frm="now-7d", variables=[VARIABLE_MODULE]) +# ===================================================================== +# 8. LA JOURNEE — le bilan condense, de minuit a maintenant +# ===================================================================== +# Les sept autres tableaux sont thematiques : on y va quand on sait deja quoi +# chercher. Celui-ci repond a la question qu'on se pose en premier — « qu'est-ce +# qui s'est passe aujourd'hui ». D'ou la fenetre `now/d` (le jour civil en +# cours) et non `now-24h`, qui melangerait deux journees et rendrait le total +# de production incomparable d'un jour a l'autre. +# +# Toutes les energies sont obtenues en INTEGRANT la puissance, jamais en lisant +# les compteurs kWh du GX : ceux-ci se remettent a zero sans le signaler +# (verifie le 04/09, -373 kWh enregistres sur une heure cote Home Assistant). + +BILAN_CLES = [ + ('sensor.victron_pv_power_system', 'pv'), + ('sensor.victron_consumption_power_l1', 'maison'), + ('sensor.victron_grid_power_l1', 'reseau'), + ('sensor.victron_evcs_ac_power', 'voiture'), + ('sensor.victron_battery_power', 'batterie'), +] +_bn = [n for _, n in BILAN_CLES] + +BILAN_BASE = ("""WITH axe AS ( + SELECT generate_series(date_trunc('minute', $__timeFrom()::timestamptz), + date_trunc('minute', $__timeTo()::timestamptz), + interval '1 minute') AS t +), +releve AS ( + SELECT date_trunc('minute', ts) AS t, +""" + + ",\n".join( + f" avg(valeur) FILTER (WHERE cle = '{c}') AS {n},\n" + f" (array_agg(valeur ORDER BY ts DESC) FILTER (WHERE cle = '{c}'))[1] AS d_{n}" + for c, n in BILAN_CLES) + """ + FROM mesure WHERE $__timeFilter(ts) GROUP BY 1 +), +par_minute AS ( + SELECT axe.t, """ + ", ".join(f"releve.{n}, releve.d_{n}" for n in _bn) + """ + FROM axe LEFT JOIN releve USING (t) +), +paliers AS ( + SELECT t, """ + ", ".join(f"{n}, d_{n}" for n in _bn) + ",\n" + + ",\n".join(f" count(d_{n}) OVER (ORDER BY t) AS g_{n}" for n in _bn) + """ + FROM par_minute +), +report AS ( + SELECT t, +""" + ",\n".join( + f" coalesce({n}, first_value(d_{n}) OVER (PARTITION BY g_{n} ORDER BY t), 0)" + f" AS {n}" for n in _bn) + """ + FROM paliers +) +""") + +# 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. +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""" + +BILAN_FLUX = BILAN_BASE + """SELECT t AS time, + pv AS "Production solaire", + maison AS "Consommation maison", + voiture AS "Charge du vehicule", + reseau AS "Reseau", + batterie AS "Batterie (+ charge, - decharge)" +FROM report ORDER BY t""" + +journee = dashboard( + 'evcs-journee', 'La journee', + "Le bilan de la journee en cours, de minuit a maintenant : ce qui a ete " + "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")], + PG, 0, 0, 24, 5, + desc="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.", + overrides=[sur("Autonomie", [{"id": "unit", "value": "percent"}, + {"id": "thresholds", "value": seuils( + ("red", None), ("#EAB839", 60), + ("green", 85))}]), + sur("Reseau importe", [{"id": "thresholds", "value": seuils( + ("green", None), ("#EAB839", 3), + ("orange", 8))}])], + champs="/.*/"), + + dict(barchart("Energie heure par heure", [sql("A", ENERGIE_SQL, 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."), + transformations=FILTRE_VUE), + + ts("Les flux de la journee", [sql("A", BILAN_FLUX)], + PG, 0, 14, 24, 10, "watt", + desc="Les cinq puissances sur le meme axe. La batterie est signee : " + "au-dessus de zero elle se remplit, en dessous elle porte la maison. " + "Quand « Reseau » decolle alors que la production est forte, c'est " + "une fermeture du contacteur qu'il faut expliquer."), + + ts("Etat de charge, et ce que la nuit exigera", [ + sql("A", "SELECT ts AS time, valeur AS \"SOC reel\"\n" + "FROM mesure WHERE cle = 'sensor.victron_battery_soc'\n" + " AND $__timeFilter(ts) ORDER BY 1"), + sql("B", "SELECT ts AS time, soc_requis_pct AS \"SOC requis par le bilan\"\n" + "FROM bilan WHERE $__timeFilter(ts) AND statut = 'ok' ORDER BY 1")], + PG, 0, 24, 12, 9, "percent", + desc="Le SOC reel contre ce que le bilan energetique reclame pour " + "traverser la nuit jusqu'au croisement production/consommation. " + "Quand le reel passe sous la ligne en fin de journee, le reseau sera " + "necessaire avant le matin."), + + timeline("Reseau, source et decisions", [ + sql("A", "SELECT ts AS time, source_mode AS \"Source\"\n" + "FROM decision WHERE $__timeFilter(ts) ORDER BY 1", fmt="table"), + sql("B", "SELECT ts AS time, etat AS \"Etat du controleur\"\n" + "FROM decision WHERE $__timeFilter(ts) ORDER BY 1", fmt="table"), + sql("C", "SELECT ts AS time,\n" + " CASE WHEN pv_censure THEN 'production bridee'\n" + " ELSE 'production libre' END AS \"Production\"\n" + "FROM bilan WHERE $__timeFilter(ts) ORDER BY 1", fmt="table")], + PG, 12, 24, 12, 9, + desc="Trois bandes qui se lisent ensemble. « Production bridee » signale " + "que le solaire n'avait plus de preneur : l'energie manquante ce " + "moment-la a ete refusee, pas absente."), + + table("Les charges du jour", [sql("A", + "SELECT to_char(debut AT TIME ZONE 'Europe/Paris', 'HH24:MI') AS \"Debut\",\n" + " coalesce(to_char(fin AT TIME ZONE 'Europe/Paris', 'HH24:MI'), 'en cours') AS \"Fin\",\n" + " coalesce(vehicule, '?') AS \"Vehicule\",\n" + " round(coalesce(energie_compteur_kwh, energie_kwh)::numeric, 2) AS \"Energie\",\n" + " round(coalesce(p_max_w, 0)::numeric) AS \"Pointe\",\n" + " coalesce(source_mode, '?') AS \"Source\"\n" + "FROM session_charge WHERE $__timeFilter(debut) ORDER BY debut DESC", fmt="table")], + PG, 0, 33, 12, 8, + desc="⚠️ L'etiquette du vehicule vient d'un selecteur manuel : elle est " + "fausse s'il n'a pas ete positionne au branchement. Les deux " + "voitures sont electriquement indiscernables (3 680 W contre " + "3 655 W a consigne egale).", + overrides=[sur("Energie", [{"id": "unit", "value": "kwatth"}]), + sur("Pointe", [{"id": "unit", "value": "watt"}])]), + + table("Ce qui est arrive aujourd'hui", [sql("A", + "SELECT to_char(ts AT TIME ZONE 'Europe/Paris', 'HH24:MI:SS') AS \"Heure\",\n" + " genre AS \"Genre\", resume AS \"Evenement\"\n" + "FROM evenement WHERE $__timeFilter(ts) AND genre <> 'bilan'\n" + "ORDER BY ts DESC", fmt="table")], + PG, 12, 33, 12, 8, + 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") + + base = pathlib.Path('deploy/historisation/grafana/dashboards') -for nom, d in [('nuit.json', nuit), ('solaire.json', solaire), +for nom, d in [('journee.json', journee), + ('nuit.json', nuit), ('solaire.json', solaire), ('prevision.json', prevision), ('telephone.json', telephone), ('systeme.json', systeme),