diff --git a/deploy/historisation/grafana/dashboards/nuit.json b/deploy/historisation/grafana/dashboards/nuit.json index 4924dc2..cc6f787 100644 --- a/deploy/historisation/grafana/dashboards/nuit.json +++ b/deploy/historisation/grafana/dashboards/nuit.json @@ -1,4 +1,7 @@ { + "templating": { + "list": [] + }, "uid": "evcs-nuit", "title": "La nuit", "description": "Ce que le bilan exigeait, contre ce que la batterie a reellement fait. C'est la question qui a demande une nuit d'archeologie le 03/09 et qui tient maintenant en un coup d'oeil.", diff --git a/deploy/historisation/grafana/dashboards/prevision.json b/deploy/historisation/grafana/dashboards/prevision.json index 4178a26..7d9a9f3 100644 --- a/deploy/historisation/grafana/dashboards/prevision.json +++ b/deploy/historisation/grafana/dashboards/prevision.json @@ -1,4 +1,7 @@ { + "templating": { + "list": [] + }, "uid": "evcs-prevision", "title": "Prevision contre reel", "description": "Le total de la prevision tombe juste ; sa FORME non. Sur les trois jours verifies le 04/09, la production arrive plus tot que prevu et l'apres-midi est sur-promis d'un facteur 2 a 3 apres 16 h. Ce tableau existe pour instruire cette question sur des semaines, pas sur trois jours.", diff --git a/deploy/historisation/grafana/dashboards/solaire.json b/deploy/historisation/grafana/dashboards/solaire.json index 62269e0..32467df 100644 --- a/deploy/historisation/grafana/dashboards/solaire.json +++ b/deploy/historisation/grafana/dashboards/solaire.json @@ -1,4 +1,7 @@ { + "templating": { + "list": [] + }, "uid": "evcs-solaire", "title": "Le solaire", "description": "La production mesuree n'est jamais la production disponible : sans injection, le MPPT ne produit que ce que le systeme peut absorber. Ces panneaux montrent l'ecart entre les deux.", diff --git a/deploy/historisation/grafana/dashboards/systeme.json b/deploy/historisation/grafana/dashboards/systeme.json index a8cb7a3..5679f22 100644 --- a/deploy/historisation/grafana/dashboards/systeme.json +++ b/deploy/historisation/grafana/dashboards/systeme.json @@ -1,4 +1,50 @@ { + "templating": { + "list": [ + { + "name": "vue", + "label": "Vue", + "type": "custom", + "hide": 0, + "skipUrlSync": false, + "query": "Vue generale : Solaire|Reseau|Batterie|Maison|Voiture,Consommation : Maison|Voiture,Reseau : Reseau|Maison,Batterie : Batterie|Solaire|Maison,Chargeur VE : Voiture|Solaire", + "options": [ + { + "selected": true, + "text": "Vue generale", + "value": "Solaire|Reseau|Batterie|Maison|Voiture" + }, + { + "selected": false, + "text": "Consommation", + "value": "Maison|Voiture" + }, + { + "selected": false, + "text": "Reseau", + "value": "Reseau|Maison" + }, + { + "selected": false, + "text": "Batterie", + "value": "Batterie|Solaire|Maison" + }, + { + "selected": false, + "text": "Chargeur VE", + "value": "Voiture|Solaire" + } + ], + "current": { + "selected": true, + "text": "Vue generale", + "value": "Solaire|Reseau|Batterie|Maison|Voiture" + }, + "multi": false, + "includeAll": false + } + ] + }, "uid": "evcs-systeme", "title": "Le systeme", "description": "Replique locale de la vue generale VRM. La console Victron est hebergee dans le cloud : elle disparait quand une coupure d'orage emporte Internet. Tout ce qui est affiche ici est lu sur le reseau local.", @@ -93,7 +139,7 @@ "gridPos": { "x": 0, "y": 0, - "w": 6, + "w": 5, "h": 5 }, "fieldConfig": { @@ -208,9 +254,9 @@ } ], "gridPos": { - "x": 6, + "x": 5, "y": 0, - "w": 5, + "w": 4, "h": 5 }, "fieldConfig": { @@ -333,9 +379,9 @@ } ], "gridPos": { - "x": 11, + "x": 9, "y": 0, - "w": 8, + "w": 10, "h": 5 }, "fieldConfig": { @@ -476,6 +522,10 @@ "justifyMode": "center", "percentChangeColorMode": "standard", "noValue": "—", + "text": { + "valueSize": 26, + "titleSize": 11 + }, "wideLayout": true } }, @@ -633,7 +683,7 @@ "gridPos": { "x": 0, "y": 5, - "w": 6, + "w": 5, "h": 8 }, "fieldConfig": { @@ -724,9 +774,9 @@ } ], "gridPos": { - "x": 6, + "x": 5, "y": 5, - "w": 18, + "w": 13, "h": 8 }, "fieldConfig": { @@ -850,6 +900,148 @@ } ] }, + { + "type": "stat", + "title": "Ce que le BMS impose", + "description": "« Charge max » est la limite que le BMS impose au systeme entier. Le 04/09 elle est tombee de 150 a 20 A en trois minutes, des 76 % de charge, au moment ou l'alarme de tension de cellule s'est declenchee — le parc n'etait alors qu'a 50,76 V pour 53,2 V autorises. Ce n'est pas le parc qui sature, c'est une cellule.", + "datasource": { + "type": "influxdb", + "uid": "evcs-influx" + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "influxdb", + "uid": "evcs-influx" + }, + "query": "from(bucket: \"evcs\")\n |> range(start: -48h)\n |> filter(fn: (r) => r._measurement == \"A\" and r.entity_id == \"airtria_8_us2000c_maximum_allowed_charge_current\" and r._field == \"value\")\n |> last()\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"Charge max\"})" + }, + { + "refId": "B", + "datasource": { + "type": "influxdb", + "uid": "evcs-influx" + }, + "query": "from(bucket: \"evcs\")\n |> range(start: -48h)\n |> filter(fn: (r) => r._measurement == \"sensor.airtria_8_us2000c_tension_de_cellule_elevee\" and r._field == \"state\")\n |> last()\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"Tension cellule\"})" + }, + { + "refId": "C", + "datasource": { + "type": "influxdb", + "uid": "evcs-influx" + }, + "query": "from(bucket: \"evcs\")\n |> range(start: -48h)\n |> filter(fn: (r) => r._measurement == \"sensor.airtria_8_us2000c_cell_imbalance\" and r._field == \"state\")\n |> last()\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"Desequilibre\"})" + }, + { + "refId": "D", + "datasource": { + "type": "influxdb", + "uid": "evcs-influx" + }, + "query": "from(bucket: \"evcs\")\n |> range(start: -48h)\n |> filter(fn: (r) => r._measurement == \"%\" and r.entity_id == \"airtria_8_us2000c_state_of_health\" and r._field == \"value\")\n |> last()\n |> keep(columns: [\"_time\", \"_value\"])\n |> rename(columns: {_value: \"Sante\"})" + } + ], + "gridPos": { + "x": 18, + "y": 5, + "w": 6, + "h": 8 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + }, + "mappings": [ + { + "type": "value", + "options": { + "no_alarm": { + "text": "OK", + "color": "green", + "index": 0 + }, + "warning": { + "text": "AVERTISSEMENT", + "color": "orange", + "index": 1 + }, + "alarm": { + "text": "ALARME", + "color": "red", + "index": 2 + } + } + } + ] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Charge max" + }, + "properties": [ + { + "id": "unit", + "value": "amp" + }, + { + "id": "decimals", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Sante" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 0 + } + ] + } + ] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "orientation": "horizontal", + "textMode": "auto", + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "percentChangeColorMode": "standard", + "noValue": "—", + "text": { + "valueSize": 24, + "titleSize": 11 + }, + "wideLayout": true + } + }, { "type": "timeseries", "title": "Les flux", @@ -985,12 +1177,22 @@ "mode": "multi", "sort": "desc" } - } + }, + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "pattern": "^(time|Heure|$vue)$" + } + } + } + ] }, { "type": "barchart", - "title": "Energie heure par heure", - "description": "Obtenue en integrant la puissance, PAS 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).", + "title": "Energie par periode", + "description": "Le pas suit la periode choisie : l'heure jusqu'a 4 jours, le jour jusqu'a 70, le mois au-dela. Obtenue en integrant la puissance, PAS 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).", "datasource": { "type": "grafana-postgresql-datasource", "uid": "evcs-pg" @@ -1005,7 +1207,7 @@ "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 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(t, 'DD/MM HH24') 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\nGROUP BY date_trunc('hour', t), to_char(t, 'DD/MM HH24')\nHAVING count(Solaire) > 0 OR count(Reseau) > 0 OR count(Maison) > 0 OR count(Voiture) > 0\nORDER BY date_trunc('hour', 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 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)" } ], "gridPos": { @@ -1045,7 +1247,17 @@ "mode": "multi", "sort": "none" } - } + }, + "transformations": [ + { + "id": "filterFieldsByName", + "options": { + "include": { + "pattern": "^(time|Heure|$vue)$" + } + } + } + ] } ] } diff --git a/deploy/historisation/grafana/dashboards/telephone.json b/deploy/historisation/grafana/dashboards/telephone.json index 95bbbe1..2a7fd0a 100644 --- a/deploy/historisation/grafana/dashboards/telephone.json +++ b/deploy/historisation/grafana/dashboards/telephone.json @@ -1,4 +1,7 @@ { + "templating": { + "list": [] + }, "uid": "evcs-telephone", "title": "Sur le telephone", "description": "Une colonne, pensee pour un ecran de telephone. Repond a une seule question : est-ce que la nuit qui vient va bien se passer ?", diff --git a/tools/gen-grafana-dashboards.py b/tools/gen-grafana-dashboards.py index 7d16d8e..367a158 100644 --- a/tools/gen-grafana-dashboards.py +++ b/tools/gen-grafana-dashboards.py @@ -73,7 +73,8 @@ def ts(title, targets, ds, x, y, w, h, unit="", desc="", extra=None, legend="bot p[k] = v return p -def stat(title, targets, ds, x, y, w, h, desc="", overrides=None, texte="auto"): +def stat(title, targets, ds, x, y, w, h, desc="", overrides=None, texte="auto", + taille=None, mappings=None): """Grands chiffres. Sur telephone, Grafana empile TOUS les panneaux en pleine largeur sous ~770 px : mettre plusieurs valeurs dans UN panneau est donc le seul moyen de les garder cote a cote et de limiter le defilement.""" @@ -84,7 +85,7 @@ def stat(title, targets, ds, x, y, w, h, desc="", overrides=None, texte="auto"): "color": {"mode": "thresholds"}, "thresholds": {"mode": "absolute", "steps": [{"color": "text", "value": None}]}, - "mappings": []}, "overrides": overrides or []}, + "mappings": mappings or []}, "overrides": overrides or []}, "options": {"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": False}, "orientation": "horizontal", "textMode": texte, @@ -92,6 +93,7 @@ def stat(title, targets, ds, x, y, w, h, desc="", overrides=None, texte="auto"): "justifyMode": "center", "percentChangeColorMode": "standard", "noValue": "—", + **({"text": taille} if taille else {}), "wideLayout": True}} @@ -136,8 +138,9 @@ 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"): +def dashboard(uid, title, desc, panels, refresh="1m", frm="now-24h", 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, @@ -614,7 +617,7 @@ telephone = dashboard("evcs-telephone", "Sur le telephone", ( # variation de -373 kWh sur une heure. Integrer la puissance ignore # completement ce probleme. -def flux_dernier(refid, mesure, entite, libelle, champ="value"): +def flux_dernier(refid, mesure, entite, libelle, champ="value", fenetre="6h"): """Derniere valeur connue d'une entite, renommee pour l'affichage.""" # Fenetre de 6 HEURES : un compromis, et il merite d'etre explique. # @@ -645,7 +648,7 @@ def flux_dernier(refid, mesure, entite, libelle, champ="value"): # constante. `last()` reste peu couteux sur une fenetre large. return flux(refid, f'from(bucket: "evcs")\n' - f' |> range(start: -6h)\n' + f' |> range(start: -{fenetre})\n' f' |> filter(fn: (r) => r._measurement == "{mesure}"' f' and r.entity_id == "{entite}" and r._field == "{champ}")\n' f' |> last()\n' @@ -678,9 +681,24 @@ 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 axe AS ( - SELECT generate_series(date_trunc('minute', $__timeFrom()::timestamptz), - date_trunc('minute', $__timeTo()::timestamptz), +ENERGIE_SQL = """WITH bornes AS ( + SELECT $__timeFrom()::timestamptz AS d, $__timeTo()::timestamptz 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 +-- l'heure au jour puis au mois. +pas AS ( + SELECT CASE WHEN f - d > interval '70 days' THEN 'month' + WHEN f - d > interval '4 days' THEN 'day' + ELSE 'hour' END AS unite, + CASE WHEN f - d > interval '70 days' THEN 'MM/YYYY' + WHEN f - d > interval '4 days' THEN 'DD/MM' + ELSE 'DD/MM HH24' END AS forme + FROM bornes +), +axe AS ( + SELECT generate_series(date_trunc('minute', (SELECT d FROM bornes)), + date_trunc('minute', (SELECT f FROM bornes)), interval '1 minute') AS t ), releve AS ( @@ -705,22 +723,15 @@ report AS ( for _, n in ENERGIE_CLES) + """ FROM paliers ) -SELECT to_char(t, 'DD/MM HH24') AS "Heure", +SELECT to_char(date_trunc(pas.unite, t), pas.forme) AS "Heure", """ + ",\n".join( # Energie REELLEMENT accumulee : puissance moyenne x duree observee. - # Pas la moyenne seule, qui comptait 22 minutes comme 60 et annoncait - # 5,13 kWh de reseau a 01 h le 04/09 — le service n'avait demarre qu'a - # 01:37. Pas non plus un rejet des heures incompletes, qui laissait - # toujours vide l'heure EN COURS, celle qu'on regarde le plus. - # Contrepartie assumee : une heure passee amputee par un arret du service - # sous-estime — mais un tel arret est journalise et apparait en marqueur - # sur le graphique. f' round((avg({n}) * count({n}) / 60.0 / 1000.0)::numeric, 2) AS "{n}"' for _, n in ENERGIE_CLES) + """ -FROM report -GROUP BY date_trunc('hour', t), to_char(t, 'DD/MM HH24') +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('hour', t)""" +ORDER BY date_trunc(pas.unite, t)""" # 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. @@ -757,17 +768,39 @@ def col(nom, unite, largeur=None, dec=0): props.append({"id": "custom.width", "value": largeur}) return sur(nom, props) + +# Selecteur de vue, dans l'esprit de VRM : la meme representation, restreinte +# aux grandeurs qui interessent. La valeur de chaque option est directement une +# alternative d'expression reguliere, consommee par la transformation +# « filterFieldsByName » des panneaux — d'ou le separateur « | ». +_VUES = [ + ("Vue generale", "Solaire|Reseau|Batterie|Maison|Voiture"), + ("Consommation", "Maison|Voiture"), + ("Reseau", "Reseau|Maison"), + ("Batterie", "Batterie|Solaire|Maison"), + ("Chargeur VE", "Voiture|Solaire"), +] +VARIABLE_VUE = { + "name": "vue", "label": "Vue", "type": "custom", "hide": 0, "skipUrlSync": False, + "query": ",".join(f"{t} : {v}" for t, v in _VUES), + "options": [{"selected": i == 0, "text": t, "value": v} for i, (t, v) in enumerate(_VUES)], + "current": {"selected": True, "text": _VUES[0][0], "value": _VUES[0][1]}, + "multi": False, "includeAll": False, +} +FILTRE_VUE = [{"id": "filterFieldsByName", + "options": {"include": {"pattern": "^(time|Heure|$vue)$"}}}] + systeme = dashboard("evcs-systeme", "Le systeme", ( "Replique locale de la vue generale VRM. La console Victron est hebergee " "dans le cloud : elle disparait quand une coupure d'orage emporte " "Internet. Tout ce qui est affiche ici est lu sur le reseau local."), - frm="now-24h", refresh="30s", panels=[ + frm="now-24h", refresh="30s", variables=[VARIABLE_VUE], panels=[ stat("Reseau", [ flux_dernier("A", "W", "victron_grid_power_l1", "Puissance"), flux_dernier("B", "V", "airtria_mpii_v1_input_voltage_l1", "Tension"), flux_dernier("C", "A", "gx_device_grid_current_l1", "Courant")], - IN, 0, 0, 6, 5, + IN, 0, 0, 5, 5, "Positif : le reseau alimente la maison. Le contacteur EDF est pilote par " "le workflow d'hysteresis, pas depuis ici.", overrides=[col("Puissance", "watt"), col("Tension", "volt"), col("Courant", "amp", dec=1)]), @@ -775,7 +808,7 @@ systeme = dashboard("evcs-systeme", "Le systeme", ( stat("Charges CA", [ flux_dernier("A", "W", "victron_consumption_power_l1", "Consommation"), flux_dernier("B", "Hz", "airtria_mpii_v1_output_frequency_l1", "Frequence")], - IN, 6, 0, 5, 5, + IN, 5, 0, 4, 5, "Ce que la maison consomme, voiture comprise.", overrides=[col("Consommation", "watt"), col("Frequence", "hertz", dec=1)]), @@ -785,7 +818,7 @@ systeme = dashboard("evcs-systeme", "Le systeme", ( flux_dernier("C", "V", "victron_battery_voltage", "Tension"), flux_dernier("D", "A", "victron_battery_current", "Courant"), flux_dernier("E", "°C", "victron_battery_temperature", "Temperature")], - IN, 11, 0, 8, 5, + IN, 9, 0, 10, 5, "Puissance positive : la batterie se charge. Sept modules actifs sur huit, " "un pack bloquant la charge vers 94 %.", overrides=[ @@ -793,7 +826,11 @@ systeme = dashboard("evcs-systeme", "Le systeme", ( {"id": "thresholds", "value": seuils( ("red", None), ("orange", 25), ("yellow", 40), ("green", 60))}]), col("Puissance", "watt"), col("Tension", "volt", dec=2), - col("Courant", "amp", dec=1), col("Temperature", "celsius")]), + col("Courant", "amp", dec=1), col("Temperature", "celsius")], + # Cinq valeurs dans huit colonnes : Grafana reduisait le texte jusqu'a + # l'illisible. Dix colonnes et une taille imposee, plutot que de laisser + # l'ajustement automatique decider. + taille={"valueSize": 26, "titleSize": 11}), stat("Chargeur VE", [ flux_dernier("A", "W", "victron_evcs_ac_power", "Puissance"), @@ -808,14 +845,14 @@ systeme = dashboard("evcs-systeme", "Le systeme", ( stat("Chargeur PV", [ flux_dernier("A", "W", "victron_pv_power_system", "Production"), flux_dernier("B", "W", "victron_pv_power_10m_avg", "Moyenne 10 min")], - IN, 0, 5, 6, 8, + IN, 0, 5, 5, 8, "Rappel qui vaut pour toute lecture de cette valeur : sans injection, la " "production MESUREE vaut min(ressource, consommation + absorption " "batterie). Basse ne veut pas dire mauvais temps.", overrides=[col("Production", "watt"), col("Moyenne 10 min", "watt")]), dict(table("Detail par tracker", [flux("A", TRACKERS_FLUX)], - IN, 6, 5, 18, 8, + IN, 5, 5, 13, 8, "Les 52 panneaux en quatre plans : sud-est 40 degres, trois chaines sud " "35 degres, ouest 45 degres. Le suiveur de trois panneaux ne publie pas " "de mode de fonctionnement et n'apparait donc pas ici.", @@ -835,6 +872,41 @@ systeme = dashboard("evcs-systeme", "Le systeme", ( transformations=[{"id": "organize", "options": { "indexByName": {"tracker": 0, "V": 1, "A": 2, "W": 3}}}]), + stat("Ce que le BMS impose", [ + # Fenetre de 48 h pour ce panneau, et c'est deliberement different du + # reste. Un etat d'alarme ne devient jamais « indisponible » : il se + # publie. Sa derniere valeur EST donc la valeur courante, meme vieille de + # dix heures — a l'inverse du courant reseau, dont le capteur disparait et + # laisse derriere lui un chiffre qui ment. + flux_dernier("A", "A", "airtria_8_us2000c_maximum_allowed_charge_current", "Charge max", fenetre="48h"), + flux("B", 'from(bucket: "evcs")\n' + ' |> range(start: -48h)\n' + ' |> filter(fn: (r) => r._measurement == "sensor.airtria_8_us2000c_tension_de_cellule_elevee"' + ' and r._field == "state")\n' + ' |> last()\n' + ' |> keep(columns: ["_time", "_value"])\n' + ' |> rename(columns: {_value: "Tension cellule"})'), + flux("C", 'from(bucket: "evcs")\n' + ' |> range(start: -48h)\n' + ' |> filter(fn: (r) => r._measurement == "sensor.airtria_8_us2000c_cell_imbalance"' + ' and r._field == "state")\n' + ' |> last()\n' + ' |> keep(columns: ["_time", "_value"])\n' + ' |> rename(columns: {_value: "Desequilibre"})'), + flux_dernier("D", "%", "airtria_8_us2000c_state_of_health", "Sante", fenetre="48h")], + IN, 18, 5, 6, 8, + "« Charge max » est la limite que le BMS impose au systeme entier. Le " + "04/09 elle est tombee de 150 a 20 A en trois minutes, des 76 % de charge, " + "au moment ou l'alarme de tension de cellule s'est declenchee — le parc " + "n'etait alors qu'a 50,76 V pour 53,2 V autorises. Ce n'est pas le parc " + "qui sature, c'est une cellule.", + overrides=[col("Charge max", "amp"), col("Sante", "percent")], + taille={"valueSize": 24, "titleSize": 11}, + mappings=[{"type": "value", "options": { + "no_alarm": {"text": "OK", "color": "green", "index": 0}, + "warning": {"text": "AVERTISSEMENT", "color": "orange", "index": 1}, + "alarm": {"text": "ALARME", "color": "red", "index": 2}}}]), + ts("Les flux", [ sql_grandeurs("A", [ ('sensor.victron_pv_power_system', 'Solaire'), @@ -852,13 +924,17 @@ systeme = dashboard("evcs-systeme", "Le systeme", ( sur("Batterie", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "blue"}}]), sur("Maison", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "text"}}, {"id": "custom.fillOpacity", "value": 0}]), - sur("Voiture", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}])]}), + sur("Voiture", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}])], + "transformations": FILTRE_VUE}), - barchart("Energie heure par heure", [sql("A", ENERGIE_SQL, fmt="table")], + dict(barchart("Energie par periode", [sql("A", ENERGIE_SQL, fmt="table")], PG, 0, 22, 24, 9, "kwatth", - "Obtenue en integrant la puissance, PAS 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)."), + "Le pas suit la periode choisie : l'heure jusqu'a 4 jours, le jour " + "jusqu'a 70, le mois au-dela. Obtenue en integrant la puissance, PAS 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)."), + transformations=FILTRE_VUE), ]) base = pathlib.Path('deploy/historisation/grafana/dashboards')