La prevision entre dans « La journee » et sur le telephone
Suite du correctif de l onglet Home Assistant. « La journee » avait le meme angle mort : cadre sur le jour civil, il s arretait a l heure en cours. Fenetre portee a la journee ENTIERE (dashboard() gagne un parametre `jusqu`), serie « Encore attendu » en gris translucide a la suite des barres mesurees, et colonne « Reste attendu » dans le bandeau du haut, a cote de « Solaire produit ». Sur le telephone, aucun panneau ajoute — un panneau de plus y est un ecran de plus a faire defiler. « Soleil restant » rejoint le bandeau Maintenant, a cote de Excedent ; « Soleil demain » rejoint La nuit qui vient, puisque c est lui qui decide jusqu ou la batterie peut descendre. Trois pieges, tous dans la meme direction. La mesure ne doit pas suivre la fenetre dans le futur : ENERGIE_SQL construisait son axe entre les deux macros puis reportait la derniere valeur connue, ce qui aurait rempli les heures a venir avec la puissance de l instant et gonfle l energie du jour — sa borne haute est desormais least($__timeTo(), now()). FULL OUTER JOIN et non LEFT : une periode a venir n a aucune ligne de mesure. Et borne haute stricte, sans quoi une barre « 18/09 00 h » apparaissait dans La journee. L heure EN COURS n a volontairement pas de barre de prevision : elle est deja mesuree pour ses minutes ecoulees. 73/73 requetes valides — mais ce controle prouve l execution, pas la grandeur. Les deux requetes de La journee ont donc ete rejouees a la main avec la vraie fenetre : 24 lignes, mesures jusqu a 02 h, prevision de 03 h a 23 h, somme des barres 36,95 kWh contre 37,0 annonces par le bandeau. C est ce recoupement qui a fait apparaitre le troisieme piege. Corrige au passage la note du 08/09 sur l acces a l API Grafana : les apostrophes du .env sont SIMPLES et non doubles, verifie a l octet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
@@ -130,7 +130,7 @@ def table(title, targets, ds, x, y, w, h, desc="", overrides=None):
|
||||
"options": {"showHeader": True, "cellHeight": "sm",
|
||||
"footer": {"show": False}}}
|
||||
|
||||
def barchart(title, targets, ds, x, y, w, h, unit="", desc=""):
|
||||
def barchart(title, targets, ds, x, y, w, h, unit="", desc="", overrides=None):
|
||||
return {"type": "barchart", "title": title, "description": desc,
|
||||
"datasource": ds, "targets": targets,
|
||||
"gridPos": {"x": x, "y": y, "w": w, "h": h},
|
||||
@@ -138,7 +138,7 @@ def barchart(title, targets, ds, x, y, w, h, unit="", desc=""):
|
||||
"color": {"mode": "palette-classic"},
|
||||
"custom": {"lineWidth": 1, "fillOpacity": 80,
|
||||
"gradientMode": "none", "axisBorderShow": False}},
|
||||
"overrides": []},
|
||||
"overrides": overrides or []},
|
||||
"options": {"orientation": "auto", "xTickLabelRotation": -45,
|
||||
"showValue": "never", "stacking": "none",
|
||||
"groupWidth": 0.7, "barWidth": 0.9,
|
||||
@@ -146,13 +146,18 @@ def barchart(title, targets, ds, x, y, w, h, unit="", desc=""):
|
||||
"showLegend": True},
|
||||
"tooltip": {"mode": "multi", "sort": "none"}}}
|
||||
|
||||
def dashboard(uid, title, desc, panels, refresh="1m", frm="now-24h", variables=None):
|
||||
def dashboard(uid, title, desc, panels, refresh="1m", frm="now-24h",
|
||||
jusqu="now", variables=None):
|
||||
return {
|
||||
"templating": {"list": variables or []},
|
||||
"uid": uid, "title": title, "description": desc,
|
||||
"tags": ["evcs"], "timezone": "Europe/Paris", "editable": True,
|
||||
"schemaVersion": 39, "version": 1, "refresh": refresh,
|
||||
"time": {"from": frm, "to": "now"},
|
||||
# `jusqu` depasse « maintenant » sur les tableaux qui montrent une
|
||||
# prevision : sans cela la partie a venir tombe hors du cadre, et le
|
||||
# tableau ne montre que du passe. Les requetes de MESURE bornent alors
|
||||
# elles-memes leur fin a `now()` — voir energie_sql().
|
||||
"time": {"from": frm, "to": jusqu},
|
||||
"timepicker": {"refresh_intervals": ["30s", "1m", "5m", "15m", "1h"]},
|
||||
"annotations": {"list": [
|
||||
{"builtIn": 1, "datasource": {"type": "grafana", "uid": "-- Grafana --"},
|
||||
@@ -179,6 +184,23 @@ def dashboard(uid, title, desc, panels, refresh="1m", frm="now-24h", variables=N
|
||||
"AND genre <> 'bilan' ORDER BY 1"}}]},
|
||||
"panels": panels}
|
||||
|
||||
# La prevision LA PLUS RECENTE, mise a plat en (heure locale, kWh), et bornee
|
||||
# aux heures STRICTEMENT a venir.
|
||||
#
|
||||
# ⚠️ L'heure EN COURS est volontairement exclue : elle est deja mesuree pour ses
|
||||
# minutes ecoulees. Y superposer une barre de prevision d'heure pleine ferait
|
||||
# compter deux fois la meme heure et donnerait un total faux.
|
||||
PREVISION_A_VENIR = """
|
||||
SELECT (c.jour + (u.i - 1) * interval '1 hour') AT TIME ZONE 'Europe/Paris' AS heure,
|
||||
u.v AS kwh
|
||||
FROM (SELECT DISTINCT ON (jour) jour, horaire FROM prevision
|
||||
WHERE jour >= current_date ORDER BY jour, publie_a DESC) c,
|
||||
unnest(c.horaire) WITH ORDINALITY AS u(v, i)
|
||||
WHERE (c.jour + (u.i - 1) * interval '1 hour') AT TIME ZONE 'Europe/Paris'
|
||||
> date_trunc('hour', now())
|
||||
"""
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# 1. LA NUIT — la question a laquelle on ne savait pas repondre le 03/09
|
||||
# =====================================================================
|
||||
@@ -541,10 +563,17 @@ telephone = dashboard("evcs-telephone", "Sur le telephone", (
|
||||
" (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_pv_power_system'\n"
|
||||
" ORDER BY ts DESC LIMIT 1)\n"
|
||||
"- (SELECT valeur FROM mesure WHERE cle = 'sensor.victron_consumption_power_l1'\n"
|
||||
" ORDER BY ts DESC LIMIT 1) AS \"Excedent\"", fmt="table")],
|
||||
" ORDER BY ts DESC LIMIT 1) AS \"Excedent\",\n"
|
||||
" (SELECT round(coalesce(sum(kwh), 0)::numeric, 1)\n"
|
||||
" FROM (" + PREVISION_A_VENIR + ") p\n"
|
||||
" WHERE p.heure < date_trunc('day', now()) + interval '1 day')\n"
|
||||
" AS \"Soleil restant\"", fmt="table")],
|
||||
PG, 0, 0, 24, 5,
|
||||
"Marge = ce que la batterie a, moins ce qu'il lui faut pour tenir jusqu'au "
|
||||
"relais du solaire. Negative, la nuit ne passe pas sans reseau.",
|
||||
"relais du solaire. Negative, la nuit ne passe pas sans reseau. "
|
||||
"« Soleil restant » est le seul chiffre de ce bandeau qui ne soit pas une "
|
||||
"mesure : c'est la derniere prevision pour les heures qui restent "
|
||||
"aujourd'hui, l'heure en cours exclue.",
|
||||
overrides=[
|
||||
sur("Batterie", [{"id": "unit", "value": "percent"},
|
||||
{"id": "thresholds", "value": seuils(
|
||||
@@ -555,22 +584,37 @@ telephone = dashboard("evcs-telephone", "Sur le telephone", (
|
||||
sur("Excedent", [{"id": "unit", "value": "watt"},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "thresholds", "value": seuils(
|
||||
("blue", None), ("green", 0))}])]),
|
||||
("blue", None), ("green", 0))}]),
|
||||
sur("Soleil restant", [{"id": "unit", "value": "kwatth"},
|
||||
{"id": "decimals", "value": 1},
|
||||
{"id": "color",
|
||||
"value": {"mode": "fixed", "fixedColor": "#9e9e9e"}}])]),
|
||||
|
||||
stat("La nuit qui vient", [
|
||||
sql("A",
|
||||
"SELECT round(energie_a_couvrir_kwh::numeric, 1) AS \"A couvrir\",\n"
|
||||
" round(soc_requis_pct::numeric) AS \"SOC requis\",\n"
|
||||
" heure_croisement AS \"Relais du solaire\"\n"
|
||||
" heure_croisement AS \"Relais du solaire\",\n"
|
||||
" (SELECT round(coalesce(sum(kwh), 0)::numeric, 1)\n"
|
||||
" FROM (" + PREVISION_A_VENIR + ") p\n"
|
||||
" WHERE p.heure >= date_trunc('day', now()) + interval '1 day')\n"
|
||||
" AS \"Soleil demain\"\n"
|
||||
"FROM bilan ORDER BY ts DESC LIMIT 1", fmt="table")],
|
||||
PG, 0, 5, 24, 5,
|
||||
"Energie a couvrir d'ici l'heure ou la production repasse au-dessus de la "
|
||||
"consommation, et le niveau de batterie qu'il faudrait pour y arriver.",
|
||||
"consommation, et le niveau de batterie qu'il faudrait pour y arriver. "
|
||||
"« Soleil demain » dit de combien la batterie pourra se refaire une fois "
|
||||
"la nuit passee : c'est lui qui decide jusqu'ou on peut la laisser "
|
||||
"descendre.",
|
||||
overrides=[
|
||||
sur("A couvrir", [{"id": "unit", "value": "kwatth"}]),
|
||||
sur("SOC requis", [{"id": "unit", "value": "percent"},
|
||||
{"id": "thresholds", "value": seuils(
|
||||
("green", None), ("yellow", 60), ("orange", 85), ("red", 100))}])]),
|
||||
("green", None), ("yellow", 60), ("orange", 85), ("red", 100))}]),
|
||||
sur("Soleil demain", [{"id": "unit", "value": "kwatth"},
|
||||
{"id": "decimals", "value": 1},
|
||||
{"id": "color",
|
||||
"value": {"mode": "fixed", "fixedColor": "#9e9e9e"}}])]),
|
||||
|
||||
ts("Ce qu'elle a, ce qu'il lui faut", [
|
||||
sql("A", "SELECT ts AS time,\n"
|
||||
@@ -691,8 +735,18 @@ ENERGIE_CLES = [
|
||||
# D'ou l'axe des minutes COMPLET par `generate_series`, la valeur reportee
|
||||
# depuis le dernier point connu, puis la moyenne. Chaque heure pese alors ses
|
||||
# soixante minutes, qu'il y ait eu une mesure ou non.
|
||||
ENERGIE_SQL = """WITH bornes AS (
|
||||
SELECT $__timeFrom()::timestamptz AS d, $__timeTo()::timestamptz AS f
|
||||
|
||||
|
||||
def energie_sql(prevision=False):
|
||||
"""Energie par periode. Avec `prevision`, une colonne « Encore attendu »
|
||||
prolonge le graphique sur les heures a venir."""
|
||||
return """WITH bornes AS (
|
||||
-- ⚠️ LA MESURE NE PEUT PAS DEPASSER MAINTENANT. « La journee » s'etend
|
||||
-- desormais jusqu'a la fin du jour pour montrer la prevision ; sans ce
|
||||
-- garde-fou, le report de la derniere valeur connue (plus bas) remplirait les
|
||||
-- heures a venir avec la puissance de l'instant et gonflerait l'energie.
|
||||
SELECT $__timeFrom()::timestamptz AS d,
|
||||
least($__timeTo()::timestamptz, now()) AS f
|
||||
),
|
||||
-- Le pas s'adapte a la plage choisie. Sans cela, « 30 derniers jours » rend
|
||||
-- 720 barres illisibles — c'est exactement ce que fait VRM en passant de
|
||||
@@ -736,16 +790,43 @@ report AS (
|
||||
f" coalesce({n}, first_value(d_{n}) OVER (PARTITION BY g_{n} ORDER BY t)) AS {n}"
|
||||
for _, n in ENERGIE_CLES) + """
|
||||
FROM paliers
|
||||
)
|
||||
SELECT to_char(date_trunc(pas.unite, t), pas.forme) AS "Heure",
|
||||
),
|
||||
mesure_par_periode AS (
|
||||
SELECT date_trunc(pas.unite, t) AS b,
|
||||
""" + ",\n".join(
|
||||
# Energie REELLEMENT accumulee : puissance moyenne x duree observee.
|
||||
f' round((avg({n}) * count({n}) / 60.0 / 1000.0)::numeric, 2) AS "{n}"'
|
||||
f' round((avg({n}) * count({n}) / 60.0 / 1000.0)::numeric, 2) AS "{n}"'
|
||||
for _, n in ENERGIE_CLES) + """
|
||||
FROM report, pas
|
||||
GROUP BY date_trunc(pas.unite, t), pas.forme
|
||||
HAVING """ + " OR ".join(f"count({n}) > 0" for _, n in ENERGIE_CLES) + """
|
||||
ORDER BY date_trunc(pas.unite, t)"""
|
||||
FROM report, pas
|
||||
GROUP BY date_trunc(pas.unite, t)
|
||||
HAVING """ + " OR ".join(f"count({n}) > 0" for _, n in ENERGIE_CLES) + """
|
||||
)
|
||||
""" + ("""
|
||||
, a_venir AS (
|
||||
SELECT date_trunc((SELECT unite FROM pas), p.heure) AS b,
|
||||
sum(p.kwh) AS kwh
|
||||
FROM (""" + PREVISION_A_VENIR + """) p
|
||||
WHERE p.heure >= (SELECT d FROM bornes)
|
||||
-- Borne STRICTE : une periode qui commence pile a la fin de la fenetre
|
||||
-- appartient a la suivante. Sans cela, « La journee » affiche une barre
|
||||
-- de demain 00 h.
|
||||
AND p.heure < $__timeTo()::timestamptz
|
||||
GROUP BY 1
|
||||
)
|
||||
-- FULL OUTER : une periode a venir n'a AUCUNE ligne de mesure, une periode
|
||||
-- passee n'a aucune prevision. Ni l'une ni l'autre ne doit disparaitre.
|
||||
SELECT to_char(coalesce(m.b, a.b), (SELECT forme FROM pas)) AS "Heure",
|
||||
""" + ",\n".join(f' m."{n}"' for _, n in ENERGIE_CLES) + """,
|
||||
round(a.kwh::numeric, 2) AS "Encore attendu"
|
||||
FROM mesure_par_periode m FULL OUTER JOIN a_venir a ON a.b = m.b
|
||||
ORDER BY coalesce(m.b, a.b)""" if prevision else """
|
||||
SELECT to_char(b, (SELECT forme FROM pas)) AS "Heure",
|
||||
""" + ",\n".join(f' "{n}"' for _, n in ENERGIE_CLES) + """
|
||||
FROM mesure_par_periode
|
||||
ORDER BY b""")
|
||||
|
||||
|
||||
ENERGIE_SQL = energie_sql()
|
||||
|
||||
# Detail par tracker. Les quatre entrees du RS 450/200 et le MPPT1 ne suivent
|
||||
# pas la meme convention de nommage : deux branches, reunies puis pivotees.
|
||||
@@ -1296,7 +1377,12 @@ BILAN_TOTAUX = BILAN_BASE + """SELECT
|
||||
round((sum(greatest(batterie, 0)) / 60.0 / 1000.0)::numeric, 1) AS "Batterie chargee",
|
||||
round((sum(greatest(-batterie, 0)) / 60.0 / 1000.0)::numeric, 1) AS "Batterie rendue",
|
||||
round((100.0 * greatest(sum(maison) - sum(reseau), 0)
|
||||
/ nullif(sum(maison), 0))::numeric, 0) AS "Autonomie"
|
||||
/ nullif(sum(maison), 0))::numeric, 0) AS "Autonomie",
|
||||
-- Ce qui reste a venir aujourd'hui, a cote de ce qui est deja fait. Sous-
|
||||
-- requete independante de `report` : elle ne touche a aucun total mesure.
|
||||
(SELECT round(coalesce(sum(kwh), 0)::numeric, 1)
|
||||
FROM (""" + PREVISION_A_VENIR + """) p
|
||||
WHERE p.heure < date_trunc('day', now()) + interval '1 day') AS "Reste attendu"
|
||||
FROM report GROUP BY (1 = 1)"""
|
||||
|
||||
BILAN_FLUX = BILAN_BASE + """SELECT t AS time,
|
||||
@@ -1318,7 +1404,9 @@ journee = dashboard(
|
||||
desc="Une ligne d'en-tetes, une ligne de valeurs. L'autonomie est la part "
|
||||
"de la consommation de la maison qui n'est PAS venue du reseau. "
|
||||
"« Batterie rendue » est ce qu'elle a restitue, donc du solaire "
|
||||
"differe et non une consommation supplementaire.",
|
||||
"differe et non une consommation supplementaire. « Reste attendu » "
|
||||
"est la seule colonne qui ne soit pas une mesure : c'est ce que la "
|
||||
"derniere prevision annonce pour les heures qui restent.",
|
||||
overrides=[sur(n, [{"id": "unit", "value": "kwatth"},
|
||||
{"id": "decimals", "value": 1}]) for n in
|
||||
("Solaire produit", "Maison consommee", "Voiture",
|
||||
@@ -1330,6 +1418,13 @@ journee = dashboard(
|
||||
{"id": "thresholds", "value": seuils(
|
||||
("green", None), ("#EAB839", 3),
|
||||
("orange", 8))}]),
|
||||
sur("Reste attendu", [{"id": "unit", "value": "kwatth"},
|
||||
{"id": "decimals", "value": 1},
|
||||
{"id": "custom.cellOptions",
|
||||
"value": {"type": "color-text"}},
|
||||
{"id": "color",
|
||||
"value": {"mode": "fixed",
|
||||
"fixedColor": "#9e9e9e"}}]),
|
||||
sur("Autonomie", [{"id": "unit", "value": "percent"},
|
||||
{"id": "decimals", "value": 0},
|
||||
{"id": "custom.cellOptions",
|
||||
@@ -1342,10 +1437,19 @@ journee = dashboard(
|
||||
# nommees par la variable $vue de « Le systeme ». Sur un tableau qui n'a pas
|
||||
# cette variable, elle filtre TOUT et Grafana affiche « No numeric fields
|
||||
# found ». Ici on veut les quatre colonnes, tout le temps.
|
||||
barchart("Energie heure par heure", [sql("A", ENERGIE_SQL, fmt="table")],
|
||||
barchart("Energie heure par heure",
|
||||
[sql("A", energie_sql(prevision=True), fmt="table")],
|
||||
PG, 0, 5, 24, 9, "kwatth",
|
||||
"La forme de la journee : quand le solaire a couvert la maison, et quand "
|
||||
"il a fallu acheter. Le pas suit la periode choisie."),
|
||||
"La forme de la journee : quand le solaire a couvert la maison, quand il "
|
||||
"a fallu acheter — et, a la suite des barres mesurees, ce que la derniere "
|
||||
"prevision annonce pour les heures qui restent. L'heure EN COURS n'a pas "
|
||||
"de barre de prevision : elle est deja mesuree pour ses minutes ecoulees.",
|
||||
overrides=[
|
||||
# Gris clair et translucide : ce n'est pas une mesure, et le coup d'oeil
|
||||
# doit le dire avant la legende.
|
||||
sur("Encore attendu", [
|
||||
{"id": "color", "value": {"mode": "fixed", "fixedColor": "#9e9e9e"}},
|
||||
{"id": "custom.fillOpacity", "value": 35}])]),
|
||||
|
||||
ts("Les flux de la journee", [sql("A", BILAN_FLUX)],
|
||||
PG, 0, 14, 24, 10, "watt",
|
||||
@@ -1405,7 +1509,7 @@ journee = dashboard(
|
||||
desc="Le genre `bilan` est ecarte : ses bascules de production bridee "
|
||||
"changent plusieurs fois par heure et noieraient le reste. Elles "
|
||||
"sont lisibles dans la frise ci-dessus."),
|
||||
], frm="now/d")
|
||||
], frm="now/d", jusqu="now/d")
|
||||
|
||||
|
||||
base = pathlib.Path('deploy/historisation/grafana/dashboards')
|
||||
|
||||
Reference in New Issue
Block a user