Un quatrieme tableau, pour le telephone
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user