From ea9248e949a2eef284db4c7d44fcf58056948194 Mon Sep 17 00:00:00 2001 From: Tallagrand Thierry Date: Fri, 4 Sep 2026 09:02:28 +0200 Subject: [PATCH] Un quatrieme tableau, pour le telephone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit L'application Grafana de l'App Store ne parle qu'a Grafana Cloud ; une instance auto-hebergee se consulte au navigateur, epingle sur l'ecran d'accueil. Reste que les trois tableaux existants sont dessines pour un ecran large. Contrainte de conception, verifiee et notee dans le README : sous ~770 px, Grafana passe CHAQUE panneau en pleine largeur et les empile. Le nombre de panneaux devient le nombre d'ecrans a faire defiler. D'ou six panneaux seulement, et les chiffres regroupes dans deux panneaux "stat" — plusieurs valeurs dans un meme panneau restent cote a cote, la ou six panneaux separes auraient fait six ecrans. La reponse d'abord : batterie, marge (ce qu'elle a moins ce qu'il lui faut), excedent. Puis l'energie a couvrir et l'heure du relais solaire. Puis les deux courbes qui expliquent ces chiffres, la frise du reseau, et les huit derniers evenements. 26/26 requetes verifiees contre les bases reelles. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ --- deploy/historisation/README.md | 10 + .../grafana/dashboards/telephone.json | 674 ++++++++++++++++++ tools/gen-grafana-dashboards.py | 134 +++- 3 files changed, 817 insertions(+), 1 deletion(-) create mode 100644 deploy/historisation/grafana/dashboards/telephone.json diff --git a/deploy/historisation/README.md b/deploy/historisation/README.md index 97e10ea..029dd17 100644 --- a/deploy/historisation/README.md +++ b/deploy/historisation/README.md @@ -226,6 +226,16 @@ Mot de passe perdu : mode déclaré par chacun des sept chargeurs. - **Prévision contre réel** — le biais de forme de la prévision, question ouverte qui demande des semaines de données. +- **Sur le téléphone** — une seule colonne, six panneaux, et la réponse en + premier. À épingler sur l'écran d'accueil de l'iPhone : + `http://10.10.10.22:3001/d/evcs-telephone?kiosk` (Safari → Partager → Sur + l'écran d'accueil). `?kiosk` retire les menus. + + ⚠️ Contrainte de conception à connaître avant d'y ajouter quoi que ce soit : + sous ~770 px de large, Grafana passe **chaque** panneau en pleine largeur et + les empile. Le nombre de panneaux est donc le nombre d'écrans à faire + défiler — d'où les chiffres regroupés dans deux panneaux « stat » plutôt que + répartis en six. Les événements de la table `evenement` apparaissent en **marqueurs verticaux orange sur tous les graphiques** : une courbe sans ses événements ne diff --git a/deploy/historisation/grafana/dashboards/telephone.json b/deploy/historisation/grafana/dashboards/telephone.json new file mode 100644 index 0000000..a382e31 --- /dev/null +++ b/deploy/historisation/grafana/dashboards/telephone.json @@ -0,0 +1,674 @@ +{ + "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 ?", + "tags": [ + "evcs" + ], + "timezone": "Europe/Paris", + "editable": true, + "schemaVersion": 39, + "version": 1, + "refresh": "1m", + "time": { + "from": "now-12h", + "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": true, + "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) ORDER BY 1" + } + } + ] + }, + "panels": [ + { + "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.", + "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\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\"" + } + ], + "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": "Batterie" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 25 + }, + { + "color": "yellow", + "value": 40 + }, + { + "color": "green", + "value": 60 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Marge" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "decimals", + "value": 1 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 0 + } + ] + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Excedent" + }, + "properties": [ + { + "id": "unit", + "value": "watt" + }, + { + "id": "decimals", + "value": 0 + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "blue", + "value": null + }, + { + "color": "green", + "value": 0 + } + ] + } + } + ] + } + ] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "orientation": "horizontal", + "textMode": "auto", + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "percentChangeColorMode": "standard", + "wideLayout": true + } + }, + { + "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.", + "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 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" + } + ], + "gridPos": { + "x": 0, + "y": 5, + "w": 24, + "h": 5 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + }, + "mappings": [] + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "A couvrir" + }, + "properties": [ + { + "id": "unit", + "value": "kwatth" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "SOC requis" + }, + "properties": [ + { + "id": "unit", + "value": "percent" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "yellow", + "value": 60 + }, + { + "color": "orange", + "value": 85 + }, + { + "color": "red", + "value": 100 + } + ] + } + } + ] + } + ] + }, + "options": { + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "orientation": "horizontal", + "textMode": "auto", + "colorMode": "value", + "graphMode": "none", + "justifyMode": "center", + "percentChangeColorMode": "standard", + "wideLayout": true + } + }, + { + "type": "timeseries", + "title": "Ce qu'elle a, ce qu'il lui faut", + "description": "Quand le vert passe sous l'orange, le reseau sera necessaire.", + "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,\n soc_pct AS \"Batterie\",\n soc_requis_pct AS \"Requis\"\nFROM bilan WHERE $__timeFilter(ts) ORDER BY 1" + } + ], + "gridPos": { + "x": 0, + "y": 10, + "w": 24, + "h": 7 + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 8, + "showPoints": "never", + "spanNulls": true, + "axisBorderShow": false, + "gradientMode": "opacity" + }, + "min": 0, + "max": 100 + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Batterie" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "green" + } + }, + { + "id": "custom.lineWidth", + "value": 3 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Requis" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "orange" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "type": "timeseries", + "title": "Ce qui reste apres la maison", + "description": "Au-dessus de zero, la voiture peut charger sans rien couter. En dessous, la charger revient a vider la batterie.", + "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 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" + } + ], + "gridPos": { + "x": 0, + "y": 17, + "w": 24, + "h": 7 + }, + "fieldConfig": { + "defaults": { + "unit": "watt", + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 2, + "fillOpacity": 8, + "showPoints": "never", + "spanNulls": true, + "axisBorderShow": false, + "gradientMode": "opacity" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Production" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "yellow" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Maison" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "text" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Excedent" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "green" + } + }, + { + "id": "custom.lineWidth", + "value": 3 + } + ] + } + ] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "calcs": [] + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "type": "state-timeline", + "title": "Le reseau", + "description": "Une seule bascule par nuit : c'est le but de la session unique.", + "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,\n source_mode AS \"Source\",\n CASE WHEN session_engagee THEN 'engagee' ELSE '-' END AS \"Session\"\nFROM decision WHERE $__timeFilter(ts) ORDER BY 1" + } + ], + "gridPos": { + "x": 0, + "y": 24, + "w": 24, + "h": 5 + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "lineWidth": 0, + "fillOpacity": 80, + "insertNulls": false, + "spanNulls": true + }, + "mappings": [ + { + "type": "value", + "options": { + "-": { + "color": "dark-blue", + "index": 0 + }, + "engagee": { + "color": "orange", + "index": 1 + } + } + } + ] + }, + "overrides": [] + }, + "options": { + "showValue": "auto", + "mergeValues": true, + "alignValue": "left", + "rowHeight": 0.85, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "single" + } + } + }, + { + "type": "table", + "title": "Ce qui s'est passe", + "description": "Les huit derniers evenements, quelle que soit la fenetre de temps.", + "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, 'DD/MM HH24:MI') AS \"Quand\",\n resume AS \"Quoi\"\nFROM evenement ORDER BY ts DESC LIMIT 8" + } + ], + "gridPos": { + "x": 0, + "y": 29, + "w": 24, + "h": 7 + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "left", + "cellOptions": { + "type": "auto" + }, + "filterable": true + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Quand" + }, + "properties": [ + { + "id": "custom.width", + "value": 110 + } + ] + } + ] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + } + ] +} diff --git a/tools/gen-grafana-dashboards.py b/tools/gen-grafana-dashboards.py index e7131a1..253541f 100644 --- a/tools/gen-grafana-dashboards.py +++ b/tools/gen-grafana-dashboards.py @@ -73,6 +73,27 @@ 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"): + """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.""" + return {"type": "stat", "title": title, "description": desc, + "datasource": ds, "targets": targets, + "gridPos": {"x": x, "y": y, "w": w, "h": h}, + "fieldConfig": {"defaults": { + "color": {"mode": "thresholds"}, + "thresholds": {"mode": "absolute", + "steps": [{"color": "text", "value": None}]}, + "mappings": []}, "overrides": overrides or []}, + "options": {"reduceOptions": {"calcs": ["lastNotNull"], + "fields": "", "values": False}, + "orientation": "horizontal", "textMode": texte, + "colorMode": "value", "graphMode": "none", + "justifyMode": "center", + "percentChangeColorMode": "standard", + "wideLayout": True}} + + def timeline(title, targets, ds, x, y, w, h, desc="", mappings=None): return {"type": "state-timeline", "title": title, "description": desc, "datasource": ds, "targets": targets, @@ -466,8 +487,119 @@ prevision = dashboard("evcs-prevision", "Prevision contre reel", ( "a ete faite. C'est la raison d'etre de cette table."), ]) +# ===================================================================== +# 4. TELEPHONE — une colonne, peu de panneaux, la reponse d'abord +# ===================================================================== +# Contrainte de conception : sous ~770 px de large, Grafana passe CHAQUE +# panneau en pleine largeur et les empile. Le nombre de panneaux devient donc +# le nombre d'ecrans a faire defiler. D'ou le choix de regrouper les chiffres +# dans deux panneaux « stat » plutot que d'en faire six. + +def seuils(*paires): + return {"mode": "absolute", "steps": [{"color": c, "value": v} for c, v in paires]} + +def sur(nom, props): + return {"matcher": {"id": "byName", "options": nom}, "properties": props} + +telephone = dashboard("evcs-telephone", "Sur le telephone", ( + "Une colonne, pensee pour un ecran de telephone. Repond a une seule " + "question : est-ce que la nuit qui vient va bien se passer ?"), + frm="now-12h", panels=[ + + stat("Maintenant", [ + sql("A", + "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\"", 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.", + overrides=[ + sur("Batterie", [{"id": "unit", "value": "percent"}, + {"id": "thresholds", "value": seuils( + ("red", None), ("orange", 25), ("yellow", 40), ("green", 60))}]), + sur("Marge", [{"id": "unit", "value": "percent"}, + {"id": "decimals", "value": 1}, + {"id": "thresholds", "value": seuils(("red", None), ("green", 0))}]), + sur("Excedent", [{"id": "unit", "value": "watt"}, + {"id": "decimals", "value": 0}, + {"id": "thresholds", "value": seuils( + ("blue", None), ("green", 0))}])]), + + 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" + "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.", + 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))}])]), + + ts("Ce qu'elle a, ce qu'il lui faut", [ + sql("A", "SELECT ts AS time,\n" + " soc_pct AS \"Batterie\",\n" + " soc_requis_pct AS \"Requis\"\n" + "FROM bilan WHERE $__timeFilter(ts) ORDER BY 1")], + PG, 0, 10, 24, 7, "percent", + "Quand le vert passe sous l'orange, le reseau sera necessaire.", + legend="bottom", + extra={"defaults": {"min": 0, "max": 100}, + "overrides": [ + sur("Batterie", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}, + {"id": "custom.lineWidth", "value": 3}]), + sur("Requis", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "orange"}}, + {"id": "custom.fillOpacity", "value": 0}])]}), + + ts("Ce qui reste apres la maison", [ + sql_grandeurs("A", [ + ('sensor.victron_pv_power_system', 'Production'), + ('sensor.victron_consumption_power_l1', 'Maison')], + calculees=[('Production - Maison', 'Excedent')])], + PG, 0, 17, 24, 7, "watt", + "Au-dessus de zero, la voiture peut charger sans rien couter. En dessous, " + "la charger revient a vider la batterie.", + extra={"overrides": [ + sur("Production", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "yellow"}}]), + sur("Maison", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "text"}}, + {"id": "custom.fillOpacity", "value": 0}]), + sur("Excedent", [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}, + {"id": "custom.lineWidth", "value": 3}])]}), + + timeline("Le reseau", [ + sql("A", "SELECT ts AS time,\n" + " source_mode AS \"Source\",\n" + " CASE WHEN session_engagee THEN 'engagee' ELSE '-' END AS \"Session\"\n" + "FROM decision WHERE $__timeFilter(ts) ORDER BY 1", fmt="table")], + PG, 0, 24, 24, 5, + "Une seule bascule par nuit : c'est le but de la session unique.", + mappings=[{"type": "value", "options": { + "-": {"color": "dark-blue", "index": 0}, + "engagee": {"color": "orange", "index": 1}}}]), + + table("Ce qui s'est passe", [ + sql("A", "SELECT to_char(ts, 'DD/MM HH24:MI') AS \"Quand\",\n" + " resume AS \"Quoi\"\n" + "FROM evenement ORDER BY ts DESC LIMIT 8", fmt="table")], + PG, 0, 29, 24, 7, + "Les huit derniers evenements, quelle que soit la fenetre de temps.", + overrides=[sur("Quand", [{"id": "custom.width", "value": 110}])]), +]) + base = pathlib.Path('deploy/historisation/grafana/dashboards') for nom, d in [('nuit.json', nuit), ('solaire.json', solaire), - ('prevision.json', prevision)]: + ('prevision.json', prevision), + ('telephone.json', telephone)]: (base / nom).write_text(json.dumps(d, indent=2, ensure_ascii=False) + '\n') print(f"{nom:16s} {len(d['panels'])} panneaux")