diff --git a/HANDOFF.md b/HANDOFF.md index d1a4adf..031e24e 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1053,5 +1053,21 @@ Attribut renomme `soc_requis_maintenant_pct` (+ `soc_requis_prudent_maintenant_p `marge_points`, `debut_heures_creuses`) ; aucun SOC du matin n'est publie. Verifie en reel 21:27 : `juste`, marge 0,5. +### 28/09 soir — Les nuits superposees : la soiree annonce la nuit +Demande du proprietaire : une prevision du SOC de 9 h par l'historique, pas par un +profil moyen. Sur **24 nuits** (04/09 -> 27/09), maison voiture exclue : 21:30->09:00 +mediane **10,0 kWh** (8,1 a 15,0), 21:30->01:24 mediane 4,7. **Regression nuit ~ +soiree (18:00-21:30) : nuit = 6,98 + 0,74 x soiree, r2 = 0,40** — une soiree chargee +(four) annonce une nuit chargee. 1 point de SOC = 0,30 kWh verifie sur 4 nuits sans +reseau ni voiture. Soir du 28/09 : soiree 6,27 kWh (forte) -> nuit prevue 11,6 kWh ; +estimation 9 h : ~35 % si EDF s'engage a 01:24, ~22 % sinon, 20 % touche vers 8 h si +la nuit est forte. **A verifier le 29/09 au matin.** +Grafana « La nuit » : 3 panneaux (nuage soiree/nuit avec regression et CE SOIR, +tableau des nuits, profil horaire 20 nuits contre la nuit en cours). SQL dans +`tools/grafana-nuits.sql` et `tools/grafana-profil-horaire.sql`, verifiees contre le +calcul par script (identiques). ⚠️ Rendu du nuage (xychart Grafana 11.3) non vu de mes +yeux : le proprietaire confirme. Piste ouverte : faire utiliser la regression par le +bilan (la nuit prevue suit la soiree reelle). + --- *Document mis a jour le : 28/09/2026 (~02:15) — VRM remplace Forecast.Solar (comparaison le 05/10), tenir jusqu'a la prochaine HC, prevision de nuit pour l'agent* diff --git a/deploy/historisation/grafana/dashboards/nuit.json b/deploy/historisation/grafana/dashboards/nuit.json index db08dd8..d43496c 100644 --- a/deploy/historisation/grafana/dashboards/nuit.json +++ b/deploy/historisation/grafana/dashboards/nuit.json @@ -642,6 +642,403 @@ "show": false } } + }, + { + "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": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "gridPos": { + "x": 0, + "y": 32, + "w": 14, + "h": 11 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "WITH pts AS (\n SELECT cle, ts, valeur,\n least(coalesce(lead(ts) OVER (PARTITION BY cle ORDER BY ts), now()), ts + interval '1 hour') AS fin\n FROM mesure\n WHERE cle IN ('sensor.victron_consumption_power_l1', 'sensor.victron_evcs_ac_power',\n 'sensor.victron_grid_power_l1', 'sensor.victron_pv_power_system')\n AND ts > now() - interval '40 days'\n),\njours AS (\n SELECT d::date AS j,\n (d::date + time '18:00') AT TIME ZONE 'Europe/Paris' AS s0,\n (d::date + time '21:30') AT TIME ZONE 'Europe/Paris' AS n0,\n (d::date + 1 + time '01:24') AT TIME ZONE 'Europe/Paris' AS hc,\n (d::date + 1 + time '09:00') AT TIME ZONE 'Europe/Paris' AS n1\n FROM generate_series(current_date - 35, current_date, interval '1 day') d\n),\ne AS (\n SELECT j.j,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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\n FROM jours j JOIN pts p ON p.fin > j.s0 AND p.ts < j.n1\n GROUP BY j.j\n),\nnuits AS (\n SELECT j.j, j.n0, j.n1,\n round(((coalesce(c_soir,0) - coalesce(v_soir,0)) / 3.6e6)::numeric, 2) AS soiree_kwh,\n round(((coalesce(c_hc,0) - coalesce(v_hc,0)) / 3.6e6)::numeric, 2) AS maison_hc_kwh,\n round(((coalesce(c_nuit,0) - coalesce(v_nuit,0)) / 3.6e6)::numeric, 2) AS maison_nuit_kwh,\n round((coalesce(v_nuit,0) / 3.6e6)::numeric, 2) AS voiture_kwh,\n round((coalesce(r_nuit,0) / 3.6e6)::numeric, 2) AS reseau_kwh,\n round((coalesce(pv_nuit,0) / 3.6e6)::numeric, 2) AS pv_avant_9h_kwh,\n (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,\n (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,\n (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\n FROM jours j JOIN e ON e.j = j.j\n WHERE j.n1 <= now() AND j.j >= date '2026-09-04'\n)\nSELECT soiree_kwh AS \"Soiree\", maison_nuit_kwh AS \"Nuit sans voiture\" FROM nuits WHERE voiture_kwh < 0.5 ORDER BY 1" + }, + { + "refId": "B", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "WITH pts AS (\n SELECT cle, ts, valeur,\n least(coalesce(lead(ts) OVER (PARTITION BY cle ORDER BY ts), now()), ts + interval '1 hour') AS fin\n FROM mesure\n WHERE cle IN ('sensor.victron_consumption_power_l1', 'sensor.victron_evcs_ac_power',\n 'sensor.victron_grid_power_l1', 'sensor.victron_pv_power_system')\n AND ts > now() - interval '40 days'\n),\njours AS (\n SELECT d::date AS j,\n (d::date + time '18:00') AT TIME ZONE 'Europe/Paris' AS s0,\n (d::date + time '21:30') AT TIME ZONE 'Europe/Paris' AS n0,\n (d::date + 1 + time '01:24') AT TIME ZONE 'Europe/Paris' AS hc,\n (d::date + 1 + time '09:00') AT TIME ZONE 'Europe/Paris' AS n1\n FROM generate_series(current_date - 35, current_date, interval '1 day') d\n),\ne AS (\n SELECT j.j,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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\n FROM jours j JOIN pts p ON p.fin > j.s0 AND p.ts < j.n1\n GROUP BY j.j\n),\nnuits AS (\n SELECT j.j, j.n0, j.n1,\n round(((coalesce(c_soir,0) - coalesce(v_soir,0)) / 3.6e6)::numeric, 2) AS soiree_kwh,\n round(((coalesce(c_hc,0) - coalesce(v_hc,0)) / 3.6e6)::numeric, 2) AS maison_hc_kwh,\n round(((coalesce(c_nuit,0) - coalesce(v_nuit,0)) / 3.6e6)::numeric, 2) AS maison_nuit_kwh,\n round((coalesce(v_nuit,0) / 3.6e6)::numeric, 2) AS voiture_kwh,\n round((coalesce(r_nuit,0) / 3.6e6)::numeric, 2) AS reseau_kwh,\n round((coalesce(pv_nuit,0) / 3.6e6)::numeric, 2) AS pv_avant_9h_kwh,\n (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,\n (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,\n (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\n FROM jours j JOIN e ON e.j = j.j\n WHERE j.n1 <= now() AND j.j >= date '2026-09-04'\n)\nSELECT soiree_kwh AS \"Soiree\", maison_nuit_kwh AS \"Nuit avec voiture\" FROM nuits WHERE voiture_kwh >= 0.5 ORDER BY 1" + }, + { + "refId": "C", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "WITH pts AS (\n SELECT cle, ts, valeur,\n least(coalesce(lead(ts) OVER (PARTITION BY cle ORDER BY ts), now()), ts + interval '1 hour') AS fin\n FROM mesure\n WHERE cle IN ('sensor.victron_consumption_power_l1', 'sensor.victron_evcs_ac_power',\n 'sensor.victron_grid_power_l1', 'sensor.victron_pv_power_system')\n AND ts > now() - interval '40 days'\n),\njours AS (\n SELECT d::date AS j,\n (d::date + time '18:00') AT TIME ZONE 'Europe/Paris' AS s0,\n (d::date + time '21:30') AT TIME ZONE 'Europe/Paris' AS n0,\n (d::date + 1 + time '01:24') AT TIME ZONE 'Europe/Paris' AS hc,\n (d::date + 1 + time '09:00') AT TIME ZONE 'Europe/Paris' AS n1\n FROM generate_series(current_date - 35, current_date, interval '1 day') d\n),\ne AS (\n SELECT j.j,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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\n FROM jours j JOIN pts p ON p.fin > j.s0 AND p.ts < j.n1\n GROUP BY j.j\n),\nnuits AS (\n SELECT j.j, j.n0, j.n1,\n round(((coalesce(c_soir,0) - coalesce(v_soir,0)) / 3.6e6)::numeric, 2) AS soiree_kwh,\n round(((coalesce(c_hc,0) - coalesce(v_hc,0)) / 3.6e6)::numeric, 2) AS maison_hc_kwh,\n round(((coalesce(c_nuit,0) - coalesce(v_nuit,0)) / 3.6e6)::numeric, 2) AS maison_nuit_kwh,\n round((coalesce(v_nuit,0) / 3.6e6)::numeric, 2) AS voiture_kwh,\n round((coalesce(r_nuit,0) / 3.6e6)::numeric, 2) AS reseau_kwh,\n round((coalesce(pv_nuit,0) / 3.6e6)::numeric, 2) AS pv_avant_9h_kwh,\n (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,\n (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,\n (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\n FROM jours j JOIN e ON e.j = j.j\n WHERE j.n1 <= now() AND j.j >= date '2026-09-04'\n)\n\nSELECT v.x AS \"Soiree\", round((r.a + r.b * v.x)::numeric, 2) AS \"Regression\"\n FROM (SELECT regr_intercept(maison_nuit_kwh, soiree_kwh) AS a, regr_slope(maison_nuit_kwh, soiree_kwh) AS b,\n min(soiree_kwh) AS lo, max(soiree_kwh) AS hi FROM nuits) r,\n LATERAL (VALUES (r.lo), (r.hi)) v(x)\n ORDER BY 1" + }, + { + "refId": "D", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "evcs-pg" + }, + "format": "table", + "rawQuery": true, + "editorMode": "code", + "rawSql": "WITH pts AS (\n SELECT cle, ts, valeur,\n least(coalesce(lead(ts) OVER (PARTITION BY cle ORDER BY ts), now()), ts + interval '1 hour') AS fin\n FROM mesure\n WHERE cle IN ('sensor.victron_consumption_power_l1', 'sensor.victron_evcs_ac_power',\n 'sensor.victron_grid_power_l1', 'sensor.victron_pv_power_system')\n AND ts > now() - interval '40 days'\n),\njours AS (\n SELECT d::date AS j,\n (d::date + time '18:00') AT TIME ZONE 'Europe/Paris' AS s0,\n (d::date + time '21:30') AT TIME ZONE 'Europe/Paris' AS n0,\n (d::date + 1 + time '01:24') AT TIME ZONE 'Europe/Paris' AS hc,\n (d::date + 1 + time '09:00') AT TIME ZONE 'Europe/Paris' AS n1\n FROM generate_series(current_date - 35, current_date, interval '1 day') d\n),\ne AS (\n SELECT j.j,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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\n FROM jours j JOIN pts p ON p.fin > j.s0 AND p.ts < j.n1\n GROUP BY j.j\n),\nnuits AS (\n SELECT j.j, j.n0, j.n1,\n round(((coalesce(c_soir,0) - coalesce(v_soir,0)) / 3.6e6)::numeric, 2) AS soiree_kwh,\n round(((coalesce(c_hc,0) - coalesce(v_hc,0)) / 3.6e6)::numeric, 2) AS maison_hc_kwh,\n round(((coalesce(c_nuit,0) - coalesce(v_nuit,0)) / 3.6e6)::numeric, 2) AS maison_nuit_kwh,\n round((coalesce(v_nuit,0) / 3.6e6)::numeric, 2) AS voiture_kwh,\n round((coalesce(r_nuit,0) / 3.6e6)::numeric, 2) AS reseau_kwh,\n round((coalesce(pv_nuit,0) / 3.6e6)::numeric, 2) AS pv_avant_9h_kwh,\n (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,\n (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,\n (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\n FROM jours j JOIN e ON e.j = j.j\n WHERE j.n1 <= now() AND j.j >= date '2026-09-04'\n),\nf AS (\n SELECT ((CASE WHEN (now() AT TIME ZONE 'Europe/Paris')::time >= time '18:00'\n THEN (now() AT TIME ZONE 'Europe/Paris')::date\n ELSE (now() AT TIME ZONE 'Europe/Paris')::date - 1 END) + time '18:00')\n AT TIME ZONE 'Europe/Paris' AS s0\n),\nsoir AS (\n SELECT sum(p.valeur * extract(epoch FROM least(p.fin, f.s0 + interval '3 hours 30 minutes', now()) - greatest(p.ts, f.s0))\n * CASE WHEN p.cle = 'sensor.victron_evcs_ac_power' THEN -1\n WHEN p.cle = 'sensor.victron_consumption_power_l1' THEN 1 ELSE 0 END) / 3.6e6 AS kwh\n FROM pts p, f\n WHERE p.fin > f.s0 AND p.ts < least(f.s0 + interval '3 hours 30 minutes', now())\n)\nSELECT round(s.kwh::numeric, 2) AS \"Soiree\",\n round((regr_intercept(maison_nuit_kwh, soiree_kwh) + regr_slope(maison_nuit_kwh, soiree_kwh) * s.kwh)::numeric, 2) AS \"Ce soir (prevu)\"\n FROM nuits, soir s GROUP BY s.kwh" + } + ], + "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": 0 + } + }, + "x": { + "matcher": { + "id": "byName", + "options": "Soiree" + } + }, + "y": { + "matcher": { + "id": "byName", + "options": "Nuit sans voiture" + } + } + }, + { + "frame": { + "matcher": { + "id": "byIndex", + "options": 1 + } + }, + "x": { + "matcher": { + "id": "byName", + "options": "Soiree" + } + }, + "y": { + "matcher": { + "id": "byName", + "options": "Nuit avec voiture" + } + } + }, + { + "frame": { + "matcher": { + "id": "byIndex", + "options": 2 + } + }, + "x": { + "matcher": { + "id": "byName", + "options": "Soiree" + } + }, + "y": { + "matcher": { + "id": "byName", + "options": "Regression" + } + } + }, + { + "frame": { + "matcher": { + "id": "byIndex", + "options": 3 + } + }, + "x": { + "matcher": { + "id": "byName", + "options": "Soiree" + } + }, + "y": { + "matcher": { + "id": "byName", + "options": "Ce soir (prevu)" + } + } + } + ], + "legend": { + "showLegend": true, + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + } + }, + { + "type": "table", + "title": "Les nuits, une ligne par nuit", + "description": "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.", + "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": "WITH pts AS (\n SELECT cle, ts, valeur,\n least(coalesce(lead(ts) OVER (PARTITION BY cle ORDER BY ts), now()), ts + interval '1 hour') AS fin\n FROM mesure\n WHERE cle IN ('sensor.victron_consumption_power_l1', 'sensor.victron_evcs_ac_power',\n 'sensor.victron_grid_power_l1', 'sensor.victron_pv_power_system')\n AND ts > now() - interval '40 days'\n),\njours AS (\n SELECT d::date AS j,\n (d::date + time '18:00') AT TIME ZONE 'Europe/Paris' AS s0,\n (d::date + time '21:30') AT TIME ZONE 'Europe/Paris' AS n0,\n (d::date + 1 + time '01:24') AT TIME ZONE 'Europe/Paris' AS hc,\n (d::date + 1 + time '09:00') AT TIME ZONE 'Europe/Paris' AS n1\n FROM generate_series(current_date - 35, current_date, interval '1 day') d\n),\ne AS (\n SELECT j.j,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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,\n 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\n FROM jours j JOIN pts p ON p.fin > j.s0 AND p.ts < j.n1\n GROUP BY j.j\n),\nnuits AS (\n SELECT j.j, j.n0, j.n1,\n round(((coalesce(c_soir,0) - coalesce(v_soir,0)) / 3.6e6)::numeric, 2) AS soiree_kwh,\n round(((coalesce(c_hc,0) - coalesce(v_hc,0)) / 3.6e6)::numeric, 2) AS maison_hc_kwh,\n round(((coalesce(c_nuit,0) - coalesce(v_nuit,0)) / 3.6e6)::numeric, 2) AS maison_nuit_kwh,\n round((coalesce(v_nuit,0) / 3.6e6)::numeric, 2) AS voiture_kwh,\n round((coalesce(r_nuit,0) / 3.6e6)::numeric, 2) AS reseau_kwh,\n round((coalesce(pv_nuit,0) / 3.6e6)::numeric, 2) AS pv_avant_9h_kwh,\n (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,\n (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,\n (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\n FROM jours j JOIN e ON e.j = j.j\n WHERE j.n1 <= now() AND j.j >= date '2026-09-04'\n)\nSELECT 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" + } + ], + "gridPos": { + "x": 14, + "y": 32, + "w": 10, + "h": 11 + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "left", + "cellOptions": { + "type": "auto" + }, + "filterable": true + } + }, + "overrides": [] + }, + "options": { + "showHeader": true, + "cellHeight": "sm", + "footer": { + "show": false + } + } + }, + { + "type": "barchart", + "title": "Profil horaire de la maison : 20 nuits contre cette nuit", + "description": "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.", + "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": "WITH pts AS (\n SELECT cle, ts, valeur,\n least(coalesce(lead(ts) OVER (PARTITION BY cle ORDER BY ts), now()), ts + interval '1 hour') AS fin\n FROM mesure\n WHERE cle IN ('sensor.victron_consumption_power_l1', 'sensor.victron_evcs_ac_power')\n AND ts > now() - interval '22 days'\n),\nheures AS (\n SELECT h AS h0, h + interval '1 hour' AS h1\n FROM generate_series(date_trunc('hour', now() - interval '20 days'), date_trunc('hour', now()), interval '1 hour') h\n),\ne AS (\n SELECT h.h0, least(h.h1, now()) AS h1,\n sum(p.valeur * extract(epoch FROM least(p.fin, h.h1) - greatest(p.ts, h.h0))\n * CASE WHEN p.cle = 'sensor.victron_evcs_ac_power' THEN -1 ELSE 1 END) / 3.6e6 AS kwh\n FROM heures h JOIN pts p ON p.fin > h.h0 AND p.ts < h.h1\n GROUP BY 1, 2\n),\ncadre AS (\n SELECT *, extract(hour FROM h0 AT TIME ZONE 'Europe/Paris')::int AS heure,\n ((extract(hour FROM h0 AT TIME ZONE 'Europe/Paris')::int + 6) % 24) AS rang\n FROM e\n)\nSELECT lpad(heure::text, 2, '0') || ' h' AS \"Heure\",\n 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\",\n 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\",\n round((max(kwh) FILTER (WHERE h0 >= (CASE WHEN extract(hour FROM now() AT TIME ZONE 'Europe/Paris') >= 18\n THEN date_trunc('day', now() AT TIME ZONE 'Europe/Paris') + interval '18 hours'\n ELSE date_trunc('day', now() AT TIME ZONE 'Europe/Paris') - interval '6 hours' END) AT TIME ZONE 'Europe/Paris'))::numeric, 2) AS \"Cette nuit\"\n FROM cadre\n GROUP BY heure, rang\n ORDER BY rang\n" + } + ], + "gridPos": { + "x": 0, + "y": 43, + "w": 24, + "h": 9 + }, + "fieldConfig": { + "defaults": { + "unit": "kwatth", + "color": { + "mode": "palette-classic" + }, + "custom": { + "lineWidth": 1, + "fillOpacity": 80, + "gradientMode": "none", + "axisBorderShow": false + } + }, + "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" + } + } + ] + } + ] + }, + "options": { + "orientation": "auto", + "xTickLabelRotation": -45, + "showValue": "never", + "stacking": "none", + "groupWidth": 0.7, + "barWidth": 0.9, + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + } } ] } diff --git a/tools/gen-grafana-dashboards.py b/tools/gen-grafana-dashboards.py index 46ba382..b308ba8 100644 --- a/tools/gen-grafana-dashboards.py +++ b/tools/gen-grafana-dashboards.py @@ -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, ]) # ===================================================================== diff --git a/tools/grafana-nuits.sql b/tools/grafana-nuits.sql new file mode 100644 index 0000000..3ef45c7 --- /dev/null +++ b/tools/grafana-nuits.sql @@ -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' +) diff --git a/tools/grafana-profil-horaire.sql b/tools/grafana-profil-horaire.sql new file mode 100644 index 0000000..b693bbe --- /dev/null +++ b/tools/grafana-profil-horaire.sql @@ -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