Bilan : la nuit prevue suit la soiree reelle (regression sur 24 nuits), nuage Grafana corrige
Le service publie le facteur nuit prevue / nuit moyenne (sensor.evcs_nuit_maison_prevue_kwh), le bilan l'applique a son profil de 21:30 a 09:00. Diagnostics 81/81, bilan 68/68. 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:
16
HANDOFF.md
16
HANDOFF.md
@@ -1066,8 +1066,20 @@ 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).
|
||||
yeux : le proprietaire a vu « Err » -> refait en UNE requete, mapping automatique.
|
||||
**Le bilan utilise la regression (28/09 23:34, demande du proprietaire).** Le service
|
||||
d'historisation publie `sensor.evcs_nuit_maison_prevue_kwh` (21:30 -> 09:00, toutes les
|
||||
5 min ; `hors_fenetre` le jour, `indisponible` si < 15 nuits ou r2 < 0,2) avec le
|
||||
FACTEUR nuit prevue / nuit moyenne des memes donnees (le profil vient des compteurs,
|
||||
la regression des puissances : le rapport annule l'ecart de methode). Le bilan
|
||||
multiplie son profil p50 par ce facteur sur 21:30-09:00 (prorata), le p80 ne descend
|
||||
jamais (max(p80, p50 recale)), facteur borne [0,75 ; 1,45] = extremes des 24 nuits,
|
||||
perime > 30 min -> ignore. Phrase « Nuit recalee sur la soiree : conso x1,092 » dans
|
||||
la raison. Tests : diagnostics 81/81 (valeurs verifiees par un calcul Python
|
||||
independant), bilan 68/68 (verifie a la main : +0,71 kWh, +2,4 points), 2 mutations
|
||||
qui mordent. Premier calcul reel 23:35 : SOC 47, requis 50,5 (~48 sans facteur) ->
|
||||
1,05 kWh EDF a 01:24. ⚠️ Ma premiere annonce d'un facteur 1,12 etait fausse (moyenne
|
||||
calculee de tete) : 1,09.
|
||||
|
||||
---
|
||||
*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*
|
||||
|
||||
@@ -667,40 +667,7 @@
|
||||
"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"
|
||||
"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),\nr AS (SELECT regr_intercept(maison_nuit_kwh, soiree_kwh) AS a, regr_slope(maison_nuit_kwh, soiree_kwh) AS b FROM nuits)\nSELECT soiree_kwh::float8 AS \"Soiree\",\n CASE WHEN voiture_kwh < 0.5 THEN maison_nuit_kwh END::float8 AS \"Nuit sans voiture\",\n CASE WHEN voiture_kwh >= 0.5 THEN maison_nuit_kwh END::float8 AS \"Nuit avec voiture\",\n round((r.a + r.b * soiree_kwh)::numeric, 2)::float8 AS \"Regression\",\n NULL::float8 AS \"Ce soir (prevu)\"\n FROM nuits, r\nUNION ALL\nSELECT round(s.kwh::numeric, 2)::float8, NULL, NULL, round((r.a + r.b * s.kwh)::numeric, 2)::float8,\n round((r.a + r.b * s.kwh)::numeric, 2)::float8\n FROM soir s, r\nORDER BY 1"
|
||||
}
|
||||
],
|
||||
"fieldConfig": {
|
||||
@@ -793,88 +760,9 @@
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"mapping": "manual",
|
||||
"mapping": "auto",
|
||||
"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,
|
||||
|
||||
@@ -140,11 +140,57 @@ if (missing.length) {
|
||||
// - profil PRUDENT (p80) → planification : cible de nuit, dimensionnement
|
||||
// du stockage, choix qui engagent plusieurs heures.
|
||||
// ---------------------------------------------------------------------
|
||||
function buildSlots(consumption) {
|
||||
// ---------------------------------------------------------------------
|
||||
// LA NUIT PREVUE PAR LA SOIREE (28/09/2026, demande du proprietaire)
|
||||
//
|
||||
// Le profil p50 est une mediane HORAIRE : il ignore la soiree qui vient de se
|
||||
// passer. Or sur 24 nuits, la maison consomme d'autant plus de 21:30 a 09:00
|
||||
// que la soiree 18:00-21:30 a ete chargee (four) : nuit = 6,98 + 0,74 x soiree,
|
||||
// r2 = 0,40. Le service d'historisation publie le FACTEUR nuit prevue / nuit
|
||||
// moyenne (sensor.evcs_nuit_maison_prevue_kwh) ; on multiplie le profil par ce
|
||||
// facteur sur la seule fenetre 21:30 -> 09:00, au prorata des heures a cheval.
|
||||
// Le 28/09 : soiree 6,27 kWh -> facteur 1,09.
|
||||
//
|
||||
// Un FACTEUR et non des kWh : le profil vient des compteurs d'energie, la
|
||||
// regression des puissances integrees ; le rapport annule l'ecart de methode.
|
||||
// Dans les deux sens pour le profil TYPIQUE (c'est un estimateur, une soiree
|
||||
// legere allege la nuit) ; le PRUDENT ne descend jamais : max(p80, p50 recale).
|
||||
// Filet : facteur borne a [0,75 ; 1,45], les extremes des 24 nuits observees
|
||||
// (0,77 et 1,41 fois la moyenne) — hors de la, c'est une mesure fausse, pas une
|
||||
// nuit. Facteur absent, perime (> 30 min) ou hors fenetre : profil inchange.
|
||||
const NIGHT_FACTOR_MIN = 0.75;
|
||||
const NIGHT_FACTOR_MAX = 1.45;
|
||||
const nowMsNight = num(input.nowMs, Date.now());
|
||||
const nightStartMs = num(input.night_house_start_ms, NaN);
|
||||
const nightEndMs = num(input.night_house_end_ms, NaN);
|
||||
const nightFactorRaw = num(input.night_house_factor, NaN);
|
||||
const nightComputedMs = num(input.night_house_computed_ms, NaN);
|
||||
const nightModelActive =
|
||||
Number.isFinite(nightFactorRaw) && nightFactorRaw > 0 &&
|
||||
Number.isFinite(nightStartMs) && Number.isFinite(nightEndMs) && nightEndMs > nightStartMs &&
|
||||
nowMsNight >= nightStartMs && nowMsNight < nightEndMs &&
|
||||
Number.isFinite(nightComputedMs) && nowMsNight - nightComputedMs <= 30 * 60000;
|
||||
const nightFactor = nightModelActive
|
||||
? Math.min(NIGHT_FACTOR_MAX, Math.max(NIGHT_FACTOR_MIN, nightFactorRaw))
|
||||
: 1;
|
||||
function nightShare(startMs) {
|
||||
const debut = num(startMs, NaN);
|
||||
if (!nightModelActive || !Number.isFinite(debut)) return 0;
|
||||
const a = Math.max(debut, nightStartMs), b = Math.min(debut + 3600000, nightEndMs);
|
||||
return b > a ? (b - a) / 3600000 : 0;
|
||||
}
|
||||
function nightScaled(profile, slot) {
|
||||
const h = num(slot.hour, 0);
|
||||
return (profile[h] ?? 0) * (1 + (nightFactor - 1) * nightShare(slot.startMs));
|
||||
}
|
||||
|
||||
function buildSlots(consumption, prudent = false) {
|
||||
return horizon.slice(0, CFG.SEARCH_HOURS).map(slot => {
|
||||
const hour = num(slot.hour, 0);
|
||||
const productionKwh = Math.max(0, num(slot.expectedKwh, 0));
|
||||
const consumptionKwh = consumption[hour] ?? 0;
|
||||
const consumptionKwh = prudent
|
||||
? Math.max(consumption[hour] ?? 0, nightScaled(consumptionP50, slot))
|
||||
: nightScaled(consumption, slot);
|
||||
return {
|
||||
startMs: slot.startMs,
|
||||
hour,
|
||||
@@ -341,7 +387,7 @@ function coverage(slots, hcFloorPct = 0) {
|
||||
}
|
||||
|
||||
const slotsP50 = buildSlots(consumptionP50);
|
||||
const slotsP80 = buildSlots(consumptionP80);
|
||||
const slotsP80 = buildSlots(consumptionP80, true);
|
||||
const hcFloorTypicalPct = hcReachFloorPct(slotsP50, nextHcStartMs);
|
||||
const hcFloorNightPrudentPct = hcNightReachActive ? hcReachFloorPct(slotsP80, nextNightHcStartMs) : 0;
|
||||
const hcFloorPrudentPct = Math.max(hcReachFloorPct(slotsP80, nextHcStartMs), hcFloorNightPrudentPct);
|
||||
@@ -613,6 +659,14 @@ return [{
|
||||
surplusAvailableKwh: round2(surplusAfterCrossingKwh),
|
||||
},
|
||||
|
||||
// Nuit prevue par la soiree (28/09) : facteur applique au profil.
|
||||
nightModel: {
|
||||
active: nightModelActive,
|
||||
factor: Math.round(nightFactor * 1000) / 1000,
|
||||
rawFactor: Number.isFinite(nightFactorRaw) ? nightFactorRaw : null,
|
||||
forecastKwh: num(input.night_house_forecast_kwh, NaN) || null,
|
||||
},
|
||||
|
||||
// Voiture branchee en HC, depart avant la prochaine HC (28/09).
|
||||
vehicleMidday: {
|
||||
active: vehicleRuleActive,
|
||||
|
||||
@@ -154,7 +154,11 @@ const hhmm = ms => new Date(ms).toLocaleTimeString('fr-FR', { timeZone: 'Europe/
|
||||
const vehText = vm.active
|
||||
? ` Voiture (départ ${hhmm(vm.departureMs)}) : ${vm.reachableKwh} kWh à donner, solaire ${vm.solarKwh} kWh → ${vm.gridNeeded ? 'réseau justifié' : 'solaire suffisant'}.`
|
||||
: '';
|
||||
setText('input_text.evcs_balance_reason', reason + hcText + vehText + degradedNote + ndText);
|
||||
const nm = balance.nightModel || {};
|
||||
const nightText = nm.active && Math.abs(nm.factor - 1) >= 0.005
|
||||
? ` Nuit recalée sur la soirée : conso ×${String(nm.factor).replace('.', ',')}${nm.forecastKwh ? ` (${String(nm.forecastKwh).replace('.', ',')} kWh prévus 21:30-09:00)` : ''}.`
|
||||
: '';
|
||||
setText('input_text.evcs_balance_reason', reason + nightText + hcText + vehText + degradedNote + ndText);
|
||||
|
||||
// Trace longue durée, pour comparer jour après jour le seuil calculé au seuil
|
||||
// fixe encore en vigueur — c'est cette comparaison qui décidera du câblage.
|
||||
|
||||
@@ -59,6 +59,11 @@
|
||||
{%- endif -%}
|
||||
{{ {
|
||||
"ts": now().isoformat(),
|
||||
"night_house_factor": state_attr("sensor.evcs_nuit_maison_prevue_kwh", "facteur"),
|
||||
"night_house_start_ms": state_attr("sensor.evcs_nuit_maison_prevue_kwh", "debut_ms"),
|
||||
"night_house_end_ms": state_attr("sensor.evcs_nuit_maison_prevue_kwh", "fin_ms"),
|
||||
"night_house_computed_ms": (as_timestamp(state_attr("sensor.evcs_nuit_maison_prevue_kwh", "calcule_a"), 0) * 1000),
|
||||
"night_house_forecast_kwh": states("sensor.evcs_nuit_maison_prevue_kwh"),
|
||||
"vehicle_connected": is_state("binary_sensor.victron_evcs_vehicle_connected", "on"),
|
||||
"vehicle_departure_ms": (as_timestamp(vh.dep) * 1000) if vh.dep != '' else none,
|
||||
"vehicle_level_pct": states("input_number.victron_evcs_vehicle_level_pct"),
|
||||
|
||||
@@ -60,6 +60,12 @@ return [{
|
||||
// Plancher de charge HC manuelle (18/09) : oublie ici jusqu'au 28/09, le
|
||||
// bouton n'avait donc jamais atteint le bilan.
|
||||
manual_hc_floor_percent: snapshot.manual_hc_floor_percent,
|
||||
// Nuit de la maison prevue par la soiree (28/09) : facteur sur le profil.
|
||||
night_house_factor: snapshot.night_house_factor,
|
||||
night_house_start_ms: snapshot.night_house_start_ms,
|
||||
night_house_end_ms: snapshot.night_house_end_ms,
|
||||
night_house_computed_ms: snapshot.night_house_computed_ms,
|
||||
night_house_forecast_kwh: snapshot.night_house_forecast_kwh,
|
||||
// Vehicule branche a midi (28/09, JEEP/VW/ZERO) : depart du jour et besoin saisi.
|
||||
vehicle_connected: snapshot.vehicle_connected,
|
||||
vehicle_departure_ms: snapshot.vehicle_departure_ms,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Les cas PV reprennent les gammes du banc OpenClaw (HANDOFF §77) : G3, G4, G5, G7.
|
||||
// node tests/diagnostics-agent/run.mjs
|
||||
import fs from 'node:fs';
|
||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, COMPTEURS_IMPORT, COMPTEUR_EDF, MPPT_MODES, ORIGINE_PAR_SOURCE, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes } from '../../tools/diagnostics-agent.mjs';
|
||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, COMPTEURS_IMPORT, COMPTEUR_EDF, MPPT_MODES, ORIGINE_PAR_SOURCE, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from '../../tools/diagnostics-agent.mjs';
|
||||
|
||||
const LIM = 'voltage_current_limited', ACT = 'mppt_active';
|
||||
function st({ pv, conso, reseau, soc = 80, courant = 0, edf = 'off', modes }) {
|
||||
@@ -287,6 +287,40 @@ for (const [nom, r, test] of imp) {
|
||||
globalThis.__nConso = (globalThis.__nConso || 0) + pj.length;
|
||||
}
|
||||
|
||||
|
||||
// --- Nuit de la maison prevue par la soiree : les 24 nuits REELLES (04->27/09) ---
|
||||
{
|
||||
const N = [[4.11,10.29],[6.52,9.81],[4.65,9.9],[3.76,8.66],[3.53,8.55],[3.61,9.48],[3.73,8.7],[7.04,11.88],
|
||||
[2.96,10.36],[5.73,13.63],[5.39,8.93],[3.12,8.14],[4.35,12.44],[5.08,12.64],[3.74,10],[4.24,10.75],
|
||||
[4.96,12.18],[5.47,11.43],[6.68,13.85],[9.48,14.95],[7.48,9.56],[4.41,9.72],[3.67,10.04],[4.81,8.87]]
|
||||
.map(([soiree, nuit]) => ({ soiree, nuit }));
|
||||
const soir = new Date('2026-09-28T21:00:00Z'); // 23:00 a Paris
|
||||
const r = nuitMaisonPrevue(N, 6.27, soir);
|
||||
const cas = [
|
||||
// Verifie par un calcul independant (Python) : a 6,979, b 0,7362, r2 0,388,
|
||||
// moyenne 10,615, prevue 11,595, facteur 1,0924.
|
||||
['soir du 28/09 : soiree 6,27 -> nuit prevue 11,6 kWh (a 6,98, b 0,736, r2 0,39)', r,
|
||||
(x) => x.etat === 11.6 && x.attributs.a === 6.98 && x.attributs.b === 0.736 && x.attributs.r2 === 0.39],
|
||||
['facteur = prevue / moyenne des memes nuits (10,62) -> 1,092', r, (x) => x.attributs.facteur === 1.092 && x.attributs.nuit_moyenne_kwh === 10.62],
|
||||
['fenetre 21:30 -> 09:00 de la bonne nuit', r,
|
||||
(x) => x.attributs.debut_ms === Date.parse('2026-09-28T19:30:00Z') && x.attributs.fin_ms === Date.parse('2026-09-29T07:00:00Z')],
|
||||
['03:00 : c est la nuit commencee la veille', nuitMaisonPrevue(N, 6.27, new Date('2026-09-29T01:00:00Z')),
|
||||
(x) => x.etat === 11.6 && x.attributs.debut_ms === Date.parse('2026-09-28T19:30:00Z')],
|
||||
['14:00 -> hors_fenetre (pas de soiree complete)', nuitMaisonPrevue(N, 6.27, new Date('2026-09-28T12:00:00Z')),
|
||||
(x) => x.etat === 'hors_fenetre' && x.attributs.facteur === null],
|
||||
['10 nuits seulement -> indisponible', nuitMaisonPrevue(N.slice(0, 10), 6.27, soir),
|
||||
(x) => x.etat === 'indisponible' && x.attributs.facteur === null],
|
||||
['nuage sans relation (r2 < 0,2) -> indisponible', nuitMaisonPrevue(N.map((p, i) => ({ soiree: p.soiree, nuit: 10 + (i % 2) })), 6.27, soir),
|
||||
(x) => x.etat === 'indisponible'],
|
||||
['soiree illisible -> indisponible', nuitMaisonPrevue(N, NaN, soir), (x) => x.etat === 'indisponible'],
|
||||
];
|
||||
for (const [nom, x, test] of cas) {
|
||||
const ok = test(x); if (!ok) { ko++; console.log(JSON.stringify(x)); }
|
||||
console.log(`${ok ? 'OK ' : 'KO '} ${nom}`);
|
||||
}
|
||||
globalThis.__nConso = (globalThis.__nConso || 0) + cas.length;
|
||||
}
|
||||
|
||||
// --- Prevision de la nuit : releve reel du 28/09 01:25 ----------------------
|
||||
{
|
||||
const base = () => new Map(Object.entries({
|
||||
|
||||
@@ -106,6 +106,9 @@ const SUMMARIZE = {
|
||||
hcReachActive: o.hcReach ? o.hcReach.active : false,
|
||||
hcFloorPct: o.hcReach ? o.hcReach.floorPct : null,
|
||||
hcNightFloorPrudentPct: o.hcReach ? o.hcReach.nightFloorPrudentPct : null,
|
||||
// Nuit prevue par la soiree (28/09).
|
||||
nightModelActive: o.nightModel ? o.nightModel.active : false,
|
||||
nightFactor: o.nightModel ? o.nightModel.factor : null,
|
||||
// Voiture branchee en HC, depart avant la prochaine HC (28/09).
|
||||
vehicleActive: o.vehicleMidday ? o.vehicleMidday.active : false,
|
||||
vehicleNeedKwh: o.vehicleMidday ? o.vehicleMidday.needKwh : null,
|
||||
|
||||
@@ -512,4 +512,49 @@ export const balanceScenarios = [
|
||||
snapshot: jeep(snap2709('2026-09-27T12:30:00+02:00', 30, MIDI_2709, SOMBRE), 0, { vehicle_capacity_kwh: 15, vehicle_charger_kw: 0 }),
|
||||
expect: { usable: true, vehicleActive: true, vehicleReachableKwh: 15, vehicleGridNeeded: true },
|
||||
},
|
||||
// ======== LA NUIT PREVUE PAR LA SOIREE (28/09/2026) ========
|
||||
// Soir du 28/09 : soiree 6,27 kWh -> facteur 1,092 sur le profil de 21:30 a
|
||||
// 09:00. Meme snapshot avec et sans facteur : seule la consommation de nuit
|
||||
// bouge, donc le SOC requis monte.
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'bilan-nuit-soiree-reference-sans-facteur',
|
||||
label: 'Reference : 23:00, SOC 52, sans facteur de nuit',
|
||||
snapshot: snap2709('2026-09-27T23:00:00+02:00', 52, null),
|
||||
expect: { usable: true, nightModelActive: false, nightFactor: 1, energyToCoverKwh: 7.77, requiredSocPct: 47.9 },
|
||||
},
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'bilan-nuit-soiree-chargee-releve-le-requis',
|
||||
label: 'Soiree chargee (facteur 1,092) : la nuit est plus lourde, le SOC requis monte',
|
||||
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), {
|
||||
night_house_factor: 1.092, night_house_forecast_kwh: '11.59',
|
||||
night_house_start_ms: Date.parse('2026-09-27T21:30:00+02:00'), night_house_end_ms: Date.parse('2026-09-28T09:00:00+02:00'),
|
||||
night_house_computed_ms: Date.parse('2026-09-27T22:57:00+02:00') }),
|
||||
// Verifie a la main : profil 23:00->09:00 = 8,0 kWh, x 0,092 = +0,74 (0,71
|
||||
// apres l'heure ou le soleil revient) ; +2,4 points = 0,71 / 0,30. Le
|
||||
// prudent (p80) est deja au-dessus du p50 recale : il ne bouge pas.
|
||||
expect: { usable: true, nightModelActive: true, nightFactor: 1.092, energyToCoverKwh: 8.48,
|
||||
requiredSocPct: 50.3, prudentRequiredSocPct: 56.7 },
|
||||
},
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'bilan-nuit-soiree-facteur-perime',
|
||||
label: 'Facteur calcule il y a 2 h : on ne l applique pas',
|
||||
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), {
|
||||
night_house_factor: 1.092,
|
||||
night_house_start_ms: Date.parse('2026-09-27T21:30:00+02:00'), night_house_end_ms: Date.parse('2026-09-28T09:00:00+02:00'),
|
||||
night_house_computed_ms: Date.parse('2026-09-27T21:00:00+02:00') }),
|
||||
expect: { usable: true, nightModelActive: false, nightFactor: 1 },
|
||||
},
|
||||
{
|
||||
node: 'BALANCE',
|
||||
id: 'bilan-nuit-soiree-facteur-aberrant-borne',
|
||||
label: 'Facteur 3 (mesure fausse) : borne a 1,45, l extreme observe',
|
||||
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), {
|
||||
night_house_factor: 3,
|
||||
night_house_start_ms: Date.parse('2026-09-27T21:30:00+02:00'), night_house_end_ms: Date.parse('2026-09-28T09:00:00+02:00'),
|
||||
night_house_computed_ms: Date.parse('2026-09-27T22:57:00+02:00') }),
|
||||
expect: { usable: true, nightModelActive: true, nightFactor: 1.45, requiredSocPct: 59.8, prudentRequiredSocPct: 62.2 },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -677,3 +677,70 @@ export function vrmVersCourbes(paires, maintenant = new Date()) {
|
||||
updated: `${p2(d.getDate())}/${p2(d.getMonth() + 1)} ${p2(d.getHours())}:${p2(d.getMinutes())}`,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// La nuit de la maison, prevue par la soiree (28/09/2026)
|
||||
// ---------------------------------------------------------------------
|
||||
// Demande du proprietaire : « si tu superposes 20 nuits tu dois trouver un
|
||||
// nuage de points ». Sur 24 nuits (04/09 -> 27/09), l'energie consommee par la
|
||||
// maison de 21:30 a 09:00 (voiture exclue) suit celle de la soiree 18:00-21:30 :
|
||||
// nuit = 6,98 + 0,74 x soiree, r2 = 0,40. Une soiree chargee (four) annonce une
|
||||
// nuit chargee ; le profil horaire MOYEN du bilan ne le voit pas.
|
||||
//
|
||||
// Ce qui est publie n'est PAS un kWh a substituer au bilan : c'est un FACTEUR,
|
||||
// nuit prevue / nuit moyenne des MEMES donnees. Le profil du bilan vient des
|
||||
// compteurs d'energie, la regression des puissances integrees : le rapport
|
||||
// annule l'ecart de methode, que la difference ne ferait qu'ajouter.
|
||||
//
|
||||
// Garde-fous : au moins 15 nuits et r2 >= 0,2, sinon le modele ne vaut rien et
|
||||
// on le dit (`indisponible`). Hors 21:30 -> 09:00, pas de soiree complete a
|
||||
// lire : `hors_fenetre`.
|
||||
export const NUIT_MIN_NUITS = 15;
|
||||
export const NUIT_MIN_R2 = 0.2;
|
||||
|
||||
function heureLocaleMs(jour, hhmm, reference) {
|
||||
// Meme calcul de decalage que minuitLocal, pour une heure de Paris donnee.
|
||||
const decalage = new Date(reference.toLocaleString('en-US', { timeZone: 'Europe/Paris' })) -
|
||||
new Date(reference.toLocaleString('en-US', { timeZone: 'UTC' }));
|
||||
return Date.parse(`${jour}T${hhmm}:00Z`) - decalage;
|
||||
}
|
||||
|
||||
export function nuitMaisonPrevue(nuits, soireeKwh, maintenant = new Date()) {
|
||||
const jourDe = (d) => new Intl.DateTimeFormat('en-CA', { timeZone: 'Europe/Paris' }).format(d);
|
||||
const hm = Number(new Intl.DateTimeFormat('en-GB', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
.format(maintenant).replace(':', ''));
|
||||
const aujourdhui = jourDe(maintenant);
|
||||
const hier = jourDe(new Date(maintenant.getTime() - 86400000));
|
||||
const demain = jourDe(new Date(maintenant.getTime() + 86400000));
|
||||
const fenetre = hm >= 2130 ? { soir: aujourdhui, matin: demain } : hm < 900 ? { soir: hier, matin: aujourdhui } : null;
|
||||
const pts = (nuits || []).map((x) => [Number(x.soiree), Number(x.nuit)]).filter(([a, b]) => Number.isFinite(a) && Number.isFinite(b));
|
||||
const n = pts.length;
|
||||
let modele = null;
|
||||
if (n >= 2) {
|
||||
const mx = pts.reduce((s, p) => s + p[0], 0) / n, my = pts.reduce((s, p) => s + p[1], 0) / n;
|
||||
let sxy = 0, sxx = 0, syy = 0;
|
||||
for (const [x, y] of pts) { sxy += (x - mx) * (y - my); sxx += (x - mx) ** 2; syy += (y - my) ** 2; }
|
||||
if (sxx > 0 && syy > 0) {
|
||||
const b = sxy / sxx;
|
||||
modele = { a: my - b * mx, b, r2: (sxy * sxy) / (sxx * syy), moyenne: my };
|
||||
}
|
||||
}
|
||||
const r2 = (v) => Math.round(v * 100) / 100;
|
||||
const attributs = {
|
||||
nuits: n, soiree_kwh: Number.isFinite(soireeKwh) ? r2(soireeKwh) : null,
|
||||
a: modele ? r2(modele.a) : null, b: modele ? Math.round(modele.b * 1000) / 1000 : null,
|
||||
r2: modele ? r2(modele.r2) : null, nuit_moyenne_kwh: modele ? r2(modele.moyenne) : null,
|
||||
debut_ms: fenetre ? heureLocaleMs(fenetre.soir, '21:30', maintenant) : null,
|
||||
fin_ms: fenetre ? heureLocaleMs(fenetre.matin, '09:00', maintenant) : null,
|
||||
};
|
||||
if (!fenetre) return { etat: 'hors_fenetre', attributs: { ...attributs, facteur: null, statut: 'hors_fenetre' } };
|
||||
if (!modele || n < NUIT_MIN_NUITS || modele.r2 < NUIT_MIN_R2 || !Number.isFinite(soireeKwh)) {
|
||||
return { etat: 'indisponible', attributs: { ...attributs, facteur: null, statut: 'indisponible',
|
||||
explication: !Number.isFinite(soireeKwh) ? 'soiree illisible'
|
||||
: n < NUIT_MIN_NUITS ? `seulement ${n} nuits (${NUIT_MIN_NUITS} requises)`
|
||||
: `modele trop faible (r2 ${modele ? r2(modele.r2) : '?'} < ${NUIT_MIN_R2})` } };
|
||||
}
|
||||
const prevue = modele.a + modele.b * soireeKwh;
|
||||
return { etat: r2(prevue), attributs: { ...attributs, facteur: Math.round((prevue / modele.moyenne) * 1000) / 1000,
|
||||
statut: 'ok', explication: `soiree ${r2(soireeKwh)} kWh -> nuit prevue ${r2(prevue)} kWh (moyenne ${r2(modele.moyenne)}, ${n} nuits, r2 ${r2(modele.r2)})` } };
|
||||
}
|
||||
|
||||
@@ -217,13 +217,17 @@ PREVISION_A_VENIR = """
|
||||
# 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()
|
||||
NUITS_CTE = pathlib.Path(__file__).with_name('nuits-maison.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 = """,
|
||||
# UNE seule trame, colonnes X puis Y (mapping automatique : premiere colonne
|
||||
# numerique = X). La premiere version (4 requetes, mapping manuel par trame)
|
||||
# affichait « Err » dans Grafana 11.3. La droite est calculee pour CHAQUE nuit :
|
||||
# elle passe par toutes les abscisses, donc aucun trou ne la coupe.
|
||||
NUAGE = """,
|
||||
f AS (
|
||||
SELECT ((CASE WHEN (now() AT TIME ZONE 'Europe/Paris')::time >= time '18:00'
|
||||
THEN (now() AT TIME ZONE 'Europe/Paris')::date
|
||||
@@ -236,17 +240,19 @@ soir AS (
|
||||
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"""
|
||||
),
|
||||
r AS (SELECT regr_intercept(maison_nuit_kwh, soiree_kwh) AS a, regr_slope(maison_nuit_kwh, soiree_kwh) AS b FROM nuits)
|
||||
SELECT soiree_kwh::float8 AS "Soiree",
|
||||
CASE WHEN voiture_kwh < 0.5 THEN maison_nuit_kwh END::float8 AS "Nuit sans voiture",
|
||||
CASE WHEN voiture_kwh >= 0.5 THEN maison_nuit_kwh END::float8 AS "Nuit avec voiture",
|
||||
round((r.a + r.b * soiree_kwh)::numeric, 2)::float8 AS "Regression",
|
||||
NULL::float8 AS "Ce soir (prevu)"
|
||||
FROM nuits, r
|
||||
UNION ALL
|
||||
SELECT round(s.kwh::numeric, 2)::float8, NULL, NULL, round((r.a + r.b * s.kwh)::numeric, 2)::float8,
|
||||
round((r.a + r.b * s.kwh)::numeric, 2)::float8
|
||||
FROM soir s, r
|
||||
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 "
|
||||
@@ -255,13 +261,7 @@ NUIT_XY = {"type": "xychart", "title": "Soiree contre nuit : le nuage des nuits"
|
||||
"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")],
|
||||
"targets": [sql("A", NUITS_CTE.rstrip() + NUAGE, fmt="table")],
|
||||
"fieldConfig": {"defaults": {"unit": "kwatth", "color": {"mode": "palette-classic"},
|
||||
"custom": {"show": "points", "pointSize": {"fixed": 9}, "pointShape": "circle",
|
||||
"lineWidth": 2, "fillOpacity": 60}},
|
||||
@@ -276,12 +276,7 @@ NUIT_XY = {"type": "xychart", "title": "Soiree contre nuit : le nuage des nuits"
|
||||
{"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)"])],
|
||||
"options": {"mapping": "auto", "series": [{}],
|
||||
"legend": {"showLegend": True, "displayMode": "list", "placement": "bottom"},
|
||||
"tooltip": {"mode": "single", "sort": "none"}}}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import pg from 'pg';
|
||||
import { texteRapport, envoyer, donneesNuit, texteDepuis } from './rapport-nuit.mjs';
|
||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes } from './diagnostics-agent.mjs';
|
||||
import { diagnosticPv, etatCellules, productionJour, chargeSolaire, bilanNuit, etatReseau, importJour, minuitLocal, COMPTEURS_IMPORT, COMPTEUR_EDF, consoJour, COMPTEURS_CONSO, projectionSoir, previsionNuit, vrmVersCourbes, nuitMaisonPrevue } from './diagnostics-agent.mjs';
|
||||
|
||||
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const ONCE = process.argv.includes('--once');
|
||||
@@ -1994,6 +1994,43 @@ async function publierPrevisionVrm(sql) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// La nuit de la maison prevue par la soiree (28/09/2026). Les nuits closes
|
||||
// viennent de la MEME requete que le tableau Grafana « La nuit »
|
||||
// (tools/nuits-maison.sql) : un seul calcul, deux lecteurs. Le bilan lit le
|
||||
// FACTEUR (nuit prevue / nuit moyenne) pour recaler son profil de 21:30 a 09:00.
|
||||
const NUITS_SQL = fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), 'nuits-maison.sql'), 'utf8');
|
||||
const SOIREE_SQL = `
|
||||
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 > to_timestamp($1 / 1000.0) - interval '2 hours' AND ts < to_timestamp($2 / 1000.0)
|
||||
)
|
||||
SELECT sum(valeur * extract(epoch FROM least(fin, to_timestamp($2 / 1000.0)) - greatest(ts, to_timestamp($1 / 1000.0)))
|
||||
* CASE WHEN cle = 'sensor.victron_evcs_ac_power' THEN -1 ELSE 1 END) / 3.6e6 AS kwh,
|
||||
count(*) FILTER (WHERE cle = 'sensor.victron_consumption_power_l1') AS points
|
||||
FROM pts WHERE fin > to_timestamp($1 / 1000.0)`;
|
||||
let nuitMaisonPubliee = false;
|
||||
async function publierNuitMaison(client) {
|
||||
if (DRY || !client) return 0;
|
||||
const maintenant = new Date();
|
||||
const cadre = nuitMaisonPrevue([], NaN, maintenant);
|
||||
let res = cadre;
|
||||
if (cadre.etat !== 'hors_fenetre') {
|
||||
const nuits = (await client.query(NUITS_SQL + '\nSELECT soiree_kwh AS soiree, maison_nuit_kwh AS nuit FROM nuits')).rows;
|
||||
const debut = cadre.attributs.debut_ms;
|
||||
const soir = (await client.query(SOIREE_SQL, [debut - 3.5 * 3600000, debut])).rows[0];
|
||||
// Une soiree sans mesure (service arrete) ne vaut pas zero kWh.
|
||||
const soiree = soir && Number(soir.points) > 0 ? Number(soir.kwh) : NaN;
|
||||
res = nuitMaisonPrevue(nuits, soiree, maintenant);
|
||||
}
|
||||
await publierCapteur('sensor.evcs_nuit_maison_prevue_kwh', res.etat, {
|
||||
friendly_name: 'EVCS — nuit de la maison prevue par la soiree', icon: 'mdi:chart-scatter-plot',
|
||||
unit_of_measurement: typeof res.etat === 'number' ? 'kWh' : undefined, ...res.attributs,
|
||||
calcule_a: maintenant.toISOString(),
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
let bilanNuitPublie = false; // publie des le premier cycle, puis toutes les 5 min
|
||||
async function publierBilanNuit(client) {
|
||||
if (DRY || !client) return 0;
|
||||
@@ -2095,6 +2132,9 @@ async function cycle(client) {
|
||||
vrm: (DRY || (vrmPublie && new Date().getMinutes() % 15 !== 7)) ? 0 : await publierPrevisionVrm(sql).then((n) => { vrmPublie = true; return n; }).catch((e) => {
|
||||
console.error(`prevision VRM : ${e.message}`); return 0;
|
||||
}),
|
||||
nuitMaison: (DRY || (nuitMaisonPubliee && new Date().getMinutes() % 5 !== 2)) ? 0 : await publierNuitMaison(client).then((n) => { nuitMaisonPubliee = true; return n; }).catch((e) => {
|
||||
console.error(`nuit de la maison : ${e.message}`); return 0;
|
||||
}),
|
||||
bilanNuit: (DRY || (bilanNuitPublie && new Date().getMinutes() % 5 !== 1)) ? 0 : await publierBilanNuit(client).then((n) => { bilanNuitPublie = true; return n; }).catch((e) => {
|
||||
console.error(`bilan de nuit : ${e.message}`); return 0;
|
||||
}),
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user