Grafana La nuit : nuits superposees (nuage soiree/nuit + regression, tableau, profil horaire)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011uJ47NxgfpGnuuMr3f2n7Z
This commit is contained in:
@@ -204,6 +204,113 @@ PREVISION_A_VENIR = """
|
||||
# =====================================================================
|
||||
# 1. LA NUIT — la question a laquelle on ne savait pas repondre le 03/09
|
||||
# =====================================================================
|
||||
|
||||
# =====================================================================
|
||||
# 1bis. LES NUITS SUPERPOSEES (28/09/2026) — demande du proprietaire
|
||||
# =====================================================================
|
||||
# « Si tu superposes 20 nuits tu dois trouver un nuage de points. » Le bilan
|
||||
# prevoit la nuit avec un profil horaire MOYEN, aveugle a la soiree qui vient
|
||||
# de se passer ; or sur 24 nuits, une soiree chargee (four, 18:00-21:30)
|
||||
# annonce une nuit chargee (r2 = 0,40, 28/09). Ces panneaux rendent la relation
|
||||
# visible, et serviront a verifier le bilan le jour ou il l'utilisera.
|
||||
# Energie = puissance tenue jusqu'au point suivant (capteurs ecrits sur
|
||||
# changement), plafond 1 h pour qu'un trou n'extrapole rien — meme methode que
|
||||
# le rapport de nuit (§76). « Maison » = consommation AC moins la voiture.
|
||||
# Chiffres verifies contre un calcul independant (script) le 28/09 : identiques.
|
||||
NUITS_CTE = pathlib.Path(__file__).with_name('grafana-nuits.sql').read_text()
|
||||
PROFIL_SQL = pathlib.Path(__file__).with_name('grafana-profil-horaire.sql').read_text()
|
||||
|
||||
def nuits_sql(select):
|
||||
return NUITS_CTE.rstrip() + "\n" + select
|
||||
|
||||
CE_SOIR = """,
|
||||
f AS (
|
||||
SELECT ((CASE WHEN (now() AT TIME ZONE 'Europe/Paris')::time >= time '18:00'
|
||||
THEN (now() AT TIME ZONE 'Europe/Paris')::date
|
||||
ELSE (now() AT TIME ZONE 'Europe/Paris')::date - 1 END) + time '18:00')
|
||||
AT TIME ZONE 'Europe/Paris' AS s0
|
||||
),
|
||||
soir AS (
|
||||
SELECT sum(p.valeur * extract(epoch FROM least(p.fin, f.s0 + interval '3 hours 30 minutes', now()) - greatest(p.ts, f.s0))
|
||||
* CASE WHEN p.cle = 'sensor.victron_evcs_ac_power' THEN -1
|
||||
WHEN p.cle = 'sensor.victron_consumption_power_l1' THEN 1 ELSE 0 END) / 3.6e6 AS kwh
|
||||
FROM pts p, f
|
||||
WHERE p.fin > f.s0 AND p.ts < least(f.s0 + interval '3 hours 30 minutes', now())
|
||||
)
|
||||
SELECT round(s.kwh::numeric, 2) AS "Soiree",
|
||||
round((regr_intercept(maison_nuit_kwh, soiree_kwh) + regr_slope(maison_nuit_kwh, soiree_kwh) * s.kwh)::numeric, 2) AS "Ce soir (prevu)"
|
||||
FROM nuits, soir s GROUP BY s.kwh"""
|
||||
|
||||
DROITE = """
|
||||
SELECT v.x AS "Soiree", round((r.a + r.b * v.x)::numeric, 2) AS "Regression"
|
||||
FROM (SELECT regr_intercept(maison_nuit_kwh, soiree_kwh) AS a, regr_slope(maison_nuit_kwh, soiree_kwh) AS b,
|
||||
min(soiree_kwh) AS lo, max(soiree_kwh) AS hi FROM nuits) r,
|
||||
LATERAL (VALUES (r.lo), (r.hi)) v(x)
|
||||
ORDER BY 1"""
|
||||
|
||||
NUIT_XY = {"type": "xychart", "title": "Soiree contre nuit : le nuage des nuits",
|
||||
"description": ("Un point = une nuit. En abscisse ce que la maison a consomme de 18:00 a "
|
||||
"21:30 (la cuisine), en ordonnee de 21:30 a 09:00, voiture exclue. La "
|
||||
"droite orange est la regression sur toutes les nuits ; le gros point rouge "
|
||||
"est CE SOIR, place sur la droite : une prevision, pas une mesure. "
|
||||
"1 point de SOC = 0,30 kWh sur le parc actuel."),
|
||||
"datasource": PG, "gridPos": {"x": 0, "y": 32, "w": 14, "h": 11},
|
||||
"targets": [
|
||||
sql("A", nuits_sql('SELECT soiree_kwh AS "Soiree", maison_nuit_kwh AS "Nuit sans voiture" '
|
||||
'FROM nuits WHERE voiture_kwh < 0.5 ORDER BY 1'), fmt="table"),
|
||||
sql("B", nuits_sql('SELECT soiree_kwh AS "Soiree", maison_nuit_kwh AS "Nuit avec voiture" '
|
||||
'FROM nuits WHERE voiture_kwh >= 0.5 ORDER BY 1'), fmt="table"),
|
||||
sql("C", nuits_sql(DROITE), fmt="table"),
|
||||
sql("D", NUITS_CTE.rstrip() + CE_SOIR, fmt="table")],
|
||||
"fieldConfig": {"defaults": {"unit": "kwatth", "color": {"mode": "palette-classic"},
|
||||
"custom": {"show": "points", "pointSize": {"fixed": 9}, "pointShape": "circle",
|
||||
"lineWidth": 2, "fillOpacity": 60}},
|
||||
"overrides": [
|
||||
{"matcher": {"id": "byName", "options": "Nuit sans voiture"},
|
||||
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}]},
|
||||
{"matcher": {"id": "byName", "options": "Nuit avec voiture"},
|
||||
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "blue"}}]},
|
||||
{"matcher": {"id": "byName", "options": "Regression"},
|
||||
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "orange"}},
|
||||
{"id": "custom.show", "value": "lines"}]},
|
||||
{"matcher": {"id": "byName", "options": "Ce soir (prevu)"},
|
||||
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "red"}},
|
||||
{"id": "custom.pointSize", "value": {"fixed": 18}}]}]},
|
||||
"options": {"mapping": "manual",
|
||||
"series": [{"frame": {"matcher": {"id": "byIndex", "options": i}},
|
||||
"x": {"matcher": {"id": "byName", "options": "Soiree"}},
|
||||
"y": {"matcher": {"id": "byName", "options": n}}}
|
||||
for i, n in enumerate(["Nuit sans voiture", "Nuit avec voiture",
|
||||
"Regression", "Ce soir (prevu)"])],
|
||||
"legend": {"showLegend": True, "displayMode": "list", "placement": "bottom"},
|
||||
"tooltip": {"mode": "single", "sort": "none"}}}
|
||||
|
||||
NUIT_TABLE = table("Les nuits, une ligne par nuit", [
|
||||
sql("A", nuits_sql(
|
||||
'SELECT to_char(j, \'DD/MM\') AS "Nuit du", soc_2130 AS "SOC 21:30", '
|
||||
'soc_0124 AS "SOC 01:24", soc_9h AS "SOC 09:00", soiree_kwh AS "Soiree 18-21:30", '
|
||||
'maison_hc_kwh AS "Maison 21:30-01:24", maison_nuit_kwh AS "Maison 21:30-09:00", '
|
||||
'voiture_kwh AS "Voiture", reseau_kwh AS "Reseau", pv_avant_9h_kwh AS "PV avant 9h" '
|
||||
'FROM nuits ORDER BY j DESC'), fmt="table")],
|
||||
PG, 14, 32, 10, 11,
|
||||
"kWh ; « Maison » = voiture exclue. La nuit en cours apparait le lendemain a "
|
||||
"09:00, une fois close. C'est la matiere du nuage de gauche.")
|
||||
|
||||
NUIT_PROFIL = barchart("Profil horaire de la maison : 20 nuits contre cette nuit", [
|
||||
sql("A", PROFIL_SQL, fmt="table")],
|
||||
PG, 0, 43, 24, 9, "kwatth",
|
||||
"kWh consommes par la maison (voiture exclue) heure par heure, de 18 h a 17 h. "
|
||||
"Mediane et valeur haute (8 nuits sur 10 en dessous) sur 20 jours, et la nuit en "
|
||||
"cours par-dessus — l'heure en cours est partielle. C'est ce profil moyen que le "
|
||||
"bilan utilise ; une soiree au-dessus de la mediane annonce une nuit au-dessus.",
|
||||
overrides=[{"matcher": {"id": "byName", "options": "Cette nuit"},
|
||||
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "red"}}]},
|
||||
{"matcher": {"id": "byName", "options": "Mediane 20 nuits"},
|
||||
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "green"}}]},
|
||||
{"matcher": {"id": "byName", "options": "8 nuits sur 10 sous"},
|
||||
"properties": [{"id": "color", "value": {"mode": "fixed", "fixedColor": "orange"}}]}])
|
||||
|
||||
|
||||
nuit = dashboard("evcs-nuit", "La nuit", (
|
||||
"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 "
|
||||
@@ -336,6 +443,7 @@ nuit = dashboard("evcs-nuit", "La nuit", (
|
||||
"vehicule": {"color": "green", "index": 5}}}]}]},
|
||||
{"matcher": {"id": "byName", "options": "Par"},
|
||||
"properties": [{"id": "custom.width", "value": 130}]}]),
|
||||
NUIT_XY, NUIT_TABLE, NUIT_PROFIL,
|
||||
])
|
||||
|
||||
# =====================================================================
|
||||
|
||||
43
tools/grafana-nuits.sql
Normal file
43
tools/grafana-nuits.sql
Normal file
@@ -0,0 +1,43 @@
|
||||
WITH pts AS (
|
||||
SELECT cle, ts, valeur,
|
||||
least(coalesce(lead(ts) OVER (PARTITION BY cle ORDER BY ts), now()), ts + interval '1 hour') AS fin
|
||||
FROM mesure
|
||||
WHERE cle IN ('sensor.victron_consumption_power_l1', 'sensor.victron_evcs_ac_power',
|
||||
'sensor.victron_grid_power_l1', 'sensor.victron_pv_power_system')
|
||||
AND ts > now() - interval '40 days'
|
||||
),
|
||||
jours AS (
|
||||
SELECT d::date AS j,
|
||||
(d::date + time '18:00') AT TIME ZONE 'Europe/Paris' AS s0,
|
||||
(d::date + time '21:30') AT TIME ZONE 'Europe/Paris' AS n0,
|
||||
(d::date + 1 + time '01:24') AT TIME ZONE 'Europe/Paris' AS hc,
|
||||
(d::date + 1 + time '09:00') AT TIME ZONE 'Europe/Paris' AS n1
|
||||
FROM generate_series(current_date - 35, current_date, interval '1 day') d
|
||||
),
|
||||
e AS (
|
||||
SELECT j.j,
|
||||
sum(p.valeur * extract(epoch FROM least(p.fin, j.n0) - greatest(p.ts, j.s0))) FILTER (WHERE p.cle = 'sensor.victron_consumption_power_l1' AND p.fin > j.s0 AND p.ts < j.n0) AS c_soir,
|
||||
sum(p.valeur * extract(epoch FROM least(p.fin, j.n0) - greatest(p.ts, j.s0))) FILTER (WHERE p.cle = 'sensor.victron_evcs_ac_power' AND p.fin > j.s0 AND p.ts < j.n0) AS v_soir,
|
||||
sum(p.valeur * extract(epoch FROM least(p.fin, j.hc) - greatest(p.ts, j.n0))) FILTER (WHERE p.cle = 'sensor.victron_consumption_power_l1' AND p.fin > j.n0 AND p.ts < j.hc) AS c_hc,
|
||||
sum(p.valeur * extract(epoch FROM least(p.fin, j.hc) - greatest(p.ts, j.n0))) FILTER (WHERE p.cle = 'sensor.victron_evcs_ac_power' AND p.fin > j.n0 AND p.ts < j.hc) AS v_hc,
|
||||
sum(p.valeur * extract(epoch FROM least(p.fin, j.n1) - greatest(p.ts, j.n0))) FILTER (WHERE p.cle = 'sensor.victron_consumption_power_l1' AND p.fin > j.n0 AND p.ts < j.n1) AS c_nuit,
|
||||
sum(p.valeur * extract(epoch FROM least(p.fin, j.n1) - greatest(p.ts, j.n0))) FILTER (WHERE p.cle = 'sensor.victron_evcs_ac_power' AND p.fin > j.n0 AND p.ts < j.n1) AS v_nuit,
|
||||
sum(p.valeur * extract(epoch FROM least(p.fin, j.n1) - greatest(p.ts, j.n0))) FILTER (WHERE p.cle = 'sensor.victron_grid_power_l1' AND p.fin > j.n0 AND p.ts < j.n1) AS r_nuit,
|
||||
sum(p.valeur * extract(epoch FROM least(p.fin, j.n1) - greatest(p.ts, j.n0))) FILTER (WHERE p.cle = 'sensor.victron_pv_power_system' AND p.fin > j.n0 AND p.ts < j.n1) AS pv_nuit
|
||||
FROM jours j JOIN pts p ON p.fin > j.s0 AND p.ts < j.n1
|
||||
GROUP BY j.j
|
||||
),
|
||||
nuits AS (
|
||||
SELECT j.j, j.n0, j.n1,
|
||||
round(((coalesce(c_soir,0) - coalesce(v_soir,0)) / 3.6e6)::numeric, 2) AS soiree_kwh,
|
||||
round(((coalesce(c_hc,0) - coalesce(v_hc,0)) / 3.6e6)::numeric, 2) AS maison_hc_kwh,
|
||||
round(((coalesce(c_nuit,0) - coalesce(v_nuit,0)) / 3.6e6)::numeric, 2) AS maison_nuit_kwh,
|
||||
round((coalesce(v_nuit,0) / 3.6e6)::numeric, 2) AS voiture_kwh,
|
||||
round((coalesce(r_nuit,0) / 3.6e6)::numeric, 2) AS reseau_kwh,
|
||||
round((coalesce(pv_nuit,0) / 3.6e6)::numeric, 2) AS pv_avant_9h_kwh,
|
||||
(SELECT valeur FROM mesure WHERE cle = 'sensor.victron_battery_soc' AND ts <= j.n0 AND valeur > 0 ORDER BY ts DESC LIMIT 1) AS soc_2130,
|
||||
(SELECT valeur FROM mesure WHERE cle = 'sensor.victron_battery_soc' AND ts <= j.hc AND valeur > 0 ORDER BY ts DESC LIMIT 1) AS soc_0124,
|
||||
(SELECT valeur FROM mesure WHERE cle = 'sensor.victron_battery_soc' AND ts <= j.n1 AND valeur > 0 ORDER BY ts DESC LIMIT 1) AS soc_9h
|
||||
FROM jours j JOIN e ON e.j = j.j
|
||||
WHERE j.n1 <= now() AND j.j >= date '2026-09-04'
|
||||
)
|
||||
32
tools/grafana-profil-horaire.sql
Normal file
32
tools/grafana-profil-horaire.sql
Normal file
@@ -0,0 +1,32 @@
|
||||
WITH pts AS (
|
||||
SELECT cle, ts, valeur,
|
||||
least(coalesce(lead(ts) OVER (PARTITION BY cle ORDER BY ts), now()), ts + interval '1 hour') AS fin
|
||||
FROM mesure
|
||||
WHERE cle IN ('sensor.victron_consumption_power_l1', 'sensor.victron_evcs_ac_power')
|
||||
AND ts > now() - interval '22 days'
|
||||
),
|
||||
heures AS (
|
||||
SELECT h AS h0, h + interval '1 hour' AS h1
|
||||
FROM generate_series(date_trunc('hour', now() - interval '20 days'), date_trunc('hour', now()), interval '1 hour') h
|
||||
),
|
||||
e AS (
|
||||
SELECT h.h0, least(h.h1, now()) AS h1,
|
||||
sum(p.valeur * extract(epoch FROM least(p.fin, h.h1) - greatest(p.ts, h.h0))
|
||||
* CASE WHEN p.cle = 'sensor.victron_evcs_ac_power' THEN -1 ELSE 1 END) / 3.6e6 AS kwh
|
||||
FROM heures h JOIN pts p ON p.fin > h.h0 AND p.ts < h.h1
|
||||
GROUP BY 1, 2
|
||||
),
|
||||
cadre AS (
|
||||
SELECT *, extract(hour FROM h0 AT TIME ZONE 'Europe/Paris')::int AS heure,
|
||||
((extract(hour FROM h0 AT TIME ZONE 'Europe/Paris')::int + 6) % 24) AS rang
|
||||
FROM e
|
||||
)
|
||||
SELECT lpad(heure::text, 2, '0') || ' h' AS "Heure",
|
||||
round(percentile_cont(0.5) WITHIN GROUP (ORDER BY kwh) FILTER (WHERE h1 - h0 = interval '1 hour' AND h0 < date_trunc('hour', now()) - interval '18 hours')::numeric, 2) AS "Mediane 20 nuits",
|
||||
round(percentile_cont(0.8) WITHIN GROUP (ORDER BY kwh) FILTER (WHERE h1 - h0 = interval '1 hour' AND h0 < date_trunc('hour', now()) - interval '18 hours')::numeric, 2) AS "8 nuits sur 10 sous",
|
||||
round((max(kwh) FILTER (WHERE h0 >= (CASE WHEN extract(hour FROM now() AT TIME ZONE 'Europe/Paris') >= 18
|
||||
THEN date_trunc('day', now() AT TIME ZONE 'Europe/Paris') + interval '18 hours'
|
||||
ELSE date_trunc('day', now() AT TIME ZONE 'Europe/Paris') - interval '6 hours' END) AT TIME ZONE 'Europe/Paris'))::numeric, 2) AS "Cette nuit"
|
||||
FROM cadre
|
||||
GROUP BY heure, rang
|
||||
ORDER BY rang
|
||||
Reference in New Issue
Block a user