Prevision de nuit : annoncer l'achat EDF reel (maison en HC + manque batterie)

Nuit du 28-29/09 : « environ 0,14 kWh a prendre sur EDF » annonces, 4,6 importes.
0,14 etait le manque de la batterie ; une session engagee fait passer toute la
maison sur le reseau jusqu'a 07:24. Le bilan publie la maison non couverte sur
la fenetre HC de nuit (input_number.evcs_balance_night_hc_house_kwh) et
sensor.evcs_prevision_nuit annonce import_prevu_kwh = maison + manque ;
reseau_necessaire_kwh supprime. Bilan 72/72, diagnostics 85/85.

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:
Tallagrand Thierry
2026-09-29 10:46:13 +02:00
parent ebcba4637b
commit 24ccecbc02
11 changed files with 198 additions and 14 deletions

View File

@@ -1132,5 +1132,34 @@ recalee x1,092), 0,34 kWh EDF annonces -> la session de 01:24 devrait s'engager.
complete de la ZERO a mesurer pour remplacer les 15 kWh annonces ; capacite JEEP 8,7 a complete de la ZERO a mesurer pour remplacer les 15 kWh annonces ; capacite JEEP 8,7 a
affiner ; demarrage solaire de l'apres-midi toujours non couvert par le besoin de nuit. affiner ; demarrage solaire de l'apres-midi toujours non couvert par le besoin de nuit.
### 29/09 matin — Verification de la nuit, et le chiffre d'EDF annonce etait le mauvais
- **Session de 01:24 engagee** (contacteur 01:24:21 -> 07:24:29, une seule fermeture). SOC
a 01:24 = 40 (pas 45 : 5,18 kWh de maison entre 21:30 et 01:24), requis 42,1. Batterie
TENUE a 40 % toute la session (consigne 950-1 460 W, courant ~0 A) ; le requis passant
sous 40 des 02:00, pas de remontee a 42 : normal. **Import 4,61 kWh**, tout en HC.
- **SOC 9 h = 37 %** (prevu ~35 % avec EDF). Plus bas 37 % vers 08:24, recharge solaire
des 09:15.
- **Nuit 21:30-09:00 = 10,25 kWh** pour 11,6 prevus (-12 %) : la soiree forte (6,27) a
annonce une nuit forte, elle a ete moyenne. Point ajoute au nuage.
- Rapport de 07:30 envoye a 07:30:01, juste (4,6 kWh, plus bas 39 % a 07:28 — la
batterie a encore baisse ensuite jusqu'a 37).
- **Agent vocal** (transcripts : desormais dans `agents/main/agent/openclaw-agent.sqlite`,
table `transcript_events`, plus dans les `.jsonl`) : 21:16 « la batterie suffit »
(ancien capteur, avant le correctif) ; 21:30 « juste » ; 21:32/21:34 « EDF sera branche,
environ 0,14 kWh ». Verdict juste, CHIFFRE trompeur : 0,14 = le MANQUE de la batterie,
alors qu'une session engagee fait passer TOUTE la maison sur le reseau jusqu'a 07:24.
- **Correctif** (demande du proprietaire) : le bilan calcule la maison non couverte par le
solaire sur la fenetre HC de nuit en cours ou a venir (profil p50 recale sur la soiree,
depuis maintenant si la fenetre est ouverte) -> `input_number.evcs_balance_night_hc_house_kwh`
(gabarit : `night_hc_start_ms` / `night_hc_end_ms`). `sensor.evcs_prevision_nuit` :
etat = **achat prevu** (maison + manque batterie) si `edf_engage` / `reseau_necessaire`,
attributs `import_prevu_kwh`, `maison_en_heures_creuses_kwh`, `manque_batterie_kwh`,
`fin_heures_creuses` ; **`reseau_necessaire_kwh` SUPPRIME** (lu comme un achat). Phrase :
« EDF sera branche de 01:24 a 07:24 : environ 4,6 kWh achetes en heures creuses (maison
4,5, batterie 0,14), car la maison passe la nuit sur le reseau ». Bilan 72/72,
diagnostics 85/85, mutations qui mordent (une premiere ne mordait pas : scenario a
03:00 pile, horizon sans tranche anterieure -> deplace a 03:30). Fiche de l'agent a
mettre a jour par SPARK.
--- ---
*Document mis a jour le : 29/09/2026 (~00:10) — regle HC active, vehicule branche a midi (JEEP/VW/ZERO), prevision de nuit a trois verdicts, nuits superposees et bilan recale sur la soiree* *Document mis a jour le : 29/09/2026 (~10:50) — nuit du 28-29 verifiee, achat EDF annonce = maison en HC + manque batterie ; regle HC active, vehicule branche a midi (JEEP/VW/ZERO), prevision de nuit a trois verdicts, nuits superposees et bilan recale sur la soiree*

View File

@@ -113,6 +113,14 @@ input_number:
unit_of_measurement: "%" unit_of_measurement: "%"
mode: box mode: box
icon: mdi:clock-alert-outline icon: mdi:clock-alert-outline
evcs_balance_night_hc_house_kwh:
name: "Bilan — maison sur le réseau en HC de nuit (si session)"
min: 0
max: 50
step: 0.01
unit_of_measurement: "kWh"
mode: box
icon: mdi:transmission-tower-import
evcs_balance_energy_to_cover_kwh: evcs_balance_energy_to_cover_kwh:
name: "Bilan — énergie à couvrir" name: "Bilan — énergie à couvrir"
min: 0 min: 0

View File

@@ -394,6 +394,33 @@ const hcFloorPrudentPct = Math.max(hcReachFloorPct(slotsP80, nextHcStartMs), hcF
const typical = coverage(slotsP50, hcFloorTypicalPct); const typical = coverage(slotsP50, hcFloorTypicalPct);
const prudent = coverage(slotsP80, hcFloorPrudentPct); const prudent = coverage(slotsP80, hcFloorPrudentPct);
// ---------------------------------------------------------------------
// CE QUE LA SESSION DE NUIT ACHETERA AU RESEAU (29/09/2026)
//
// Incident du 28/09 : la prevision annoncait « 0,14 kWh a prendre sur EDF » et
// le reseau en a fourni 4,6. Les deux etaient justes, mais le premier est le
// MANQUE de la batterie : une fois engagee, la session tient la batterie et la
// maison passe TOUTE la fenetre HC de nuit sur le reseau (§40). Le chiffre qui
// repond a « combien vais-je acheter » est donc la maison non couverte par le
// solaire sur la fenetre (profil habituel, recale sur la soiree), depuis
// maintenant si la fenetre est ouverte. Le manque batterie s'y ajoute, a part.
// ---------------------------------------------------------------------
const nightHcStartMs = num(input.night_hc_start_ms, NaN);
const nightHcEndMs = num(input.night_hc_end_ms, NaN);
const nightHcActive = Number.isFinite(nightHcStartMs) && Number.isFinite(nightHcEndMs)
&& nightHcEndMs > nightHcStartMs;
let nightHcHouseKwh = null;
if (nightHcActive) {
const depuis = Math.max(nightHcStartMs, num(input.nowMs, Date.now()));
nightHcHouseKwh = 0;
for (const sl of slotsP50) {
const debut = num(sl.startMs, NaN);
if (!Number.isFinite(debut)) continue;
const a = Math.max(debut, depuis), b = Math.min(debut + 3600000, nightHcEndMs);
if (b > a) nightHcHouseKwh += Math.max(0, -sl.netKwh) * (b - a) / 3600000;
}
}
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
// VEHICULE BRANCHE EN HC AVEC UN DEPART AVANT LA PROCHAINE HC (28/09/2026) // VEHICULE BRANCHE EN HC AVEC UN DEPART AVANT LA PROCHAINE HC (28/09/2026)
// //
@@ -667,6 +694,14 @@ return [{
forecastKwh: num(input.night_house_forecast_kwh, NaN) || null, forecastKwh: num(input.night_house_forecast_kwh, NaN) || null,
}, },
// Maison sur le reseau pendant la fenetre HC de nuit, si la session s'engage (29/09).
nightHcImport: {
active: nightHcActive,
startMs: nightHcActive ? nightHcStartMs : null,
endMs: nightHcActive ? nightHcEndMs : null,
houseKwh: nightHcHouseKwh === null ? null : round2(nightHcHouseKwh),
},
// Voiture branchee en HC, depart avant la prochaine HC (28/09). // Voiture branchee en HC, depart avant la prochaine HC (28/09).
vehicleMidday: { vehicleMidday: {
active: vehicleRuleActive, active: vehicleRuleActive,

View File

@@ -92,6 +92,9 @@ const vm = balance.vehicleMidday || {};
setBool('input_boolean.evcs_balance_vehicle_grid_needed', !!vm.gridNeeded); setBool('input_boolean.evcs_balance_vehicle_grid_needed', !!vm.gridNeeded);
setNumber('input_number.evcs_balance_vehicle_need_kwh', vm.active ? vm.reachableKwh : 0); setNumber('input_number.evcs_balance_vehicle_need_kwh', vm.active ? vm.reachableKwh : 0);
setNumber('input_number.evcs_balance_vehicle_solar_kwh', vm.active ? vm.solarKwh : 0); setNumber('input_number.evcs_balance_vehicle_solar_kwh', vm.active ? vm.solarKwh : 0);
// Maison sur le reseau pendant la fenetre HC de nuit si la session s'engage (29/09).
const nh = balance.nightHcImport || {};
setNumber('input_number.evcs_balance_night_hc_house_kwh', nh.active && nh.houseKwh !== null ? nh.houseKwh : 0);
setNumber('input_number.evcs_balance_energy_to_cover_kwh', c.energyToCoverKwh); setNumber('input_number.evcs_balance_energy_to_cover_kwh', c.energyToCoverKwh);
setNumber('input_number.evcs_balance_grid_energy_needed_kwh', c.gridEnergyNeededKwh); setNumber('input_number.evcs_balance_grid_energy_needed_kwh', c.gridEnergyNeededKwh);
setNumber('input_number.evcs_balance_vehicle_surplus_kwh', balance.vehicle.surplusAvailableKwh); setNumber('input_number.evcs_balance_vehicle_surplus_kwh', balance.vehicle.surplusAvailableKwh);

View File

@@ -3,7 +3,7 @@
instants : la fin de la fenetre HC en cours (vide hors fenetre) et le debut instants : la fin de la fenetre HC en cours (vide hors fenetre) et le debut
de la fenetre HC suivante. Strategie du proprietaire : la batterie doit de la fenetre HC suivante. Strategie du proprietaire : la batterie doit
tenir jusqu'a la prochaine fenetre HC. -#} tenir jusqu'a la prochaine fenetre HC. -#}
{%- set ns = namespace(fin='', suivant='', nuit='', fen=[]) -%} {%- set ns = namespace(fin='', suivant='', nuit='', fen=[], nd='', nf='') -%}
{%- for j in [0, 1, 2] -%} {%- for j in [0, 1, 2] -%}
{%- for a, b, t in [('victron_evcs_hc_night_start', 'victron_evcs_hc_night_stop', 'nuit'), ('victron_evcs_hc_midday_start', 'victron_evcs_hc_midday_stop', 'midi')] -%} {%- for a, b, t in [('victron_evcs_hc_night_start', 'victron_evcs_hc_night_stop', 'nuit'), ('victron_evcs_hc_midday_start', 'victron_evcs_hc_midday_stop', 'midi')] -%}
{%- set d = today_at(states('input_datetime.' ~ a)) + timedelta(days=j) -%} {%- set d = today_at(states('input_datetime.' ~ a)) + timedelta(days=j) -%}
@@ -20,6 +20,11 @@
{%- if w[0] >= repere and ns.suivant == '' -%}{%- set ns.suivant = w[0] -%}{%- endif -%} {%- if w[0] >= repere and ns.suivant == '' -%}{%- set ns.suivant = w[0] -%}{%- endif -%}
{%- if w[0] >= repere and w[2] == 'nuit' and ns.nuit == '' -%}{%- set ns.nuit = w[0] -%}{%- endif -%} {%- if w[0] >= repere and w[2] == 'nuit' and ns.nuit == '' -%}{%- set ns.nuit = w[0] -%}{%- endif -%}
{%- endfor -%} {%- endfor -%}
{#- Fenetre HC de NUIT en cours, sinon la prochaine (29/09) : sert a prevoir
ce que la maison achetera au reseau si la session de nuit s'engage. -#}
{%- for w in ns.fen | sort(attribute='0') -%}
{%- if w[2] == 'nuit' and w[1] > now() and ns.nd == '' -%}{%- set ns.nd = w[0] -%}{%- set ns.nf = w[1] -%}{%- endif -%}
{%- endfor -%}
{#- Vehicule branche a midi (28/09/2026) — JEEP, VW ou ZERO, selon le {#- Vehicule branche a midi (28/09/2026) — JEEP, VW ou ZERO, selon le
selecteur. Pour chacun, HA porte le calendrier (jours et heure de depart selecteur. Pour chacun, HA porte le calendrier (jours et heure de depart
apres le dejeuner), la capacite et la puissance de charge MESUREES ; le apres le dejeuner), la capacite et la puissance de charge MESUREES ; le
@@ -74,6 +79,8 @@
"hc_window_end_ms": (as_timestamp(ns.fin) * 1000) if ns.fin != '' else none, "hc_window_end_ms": (as_timestamp(ns.fin) * 1000) if ns.fin != '' else none,
"next_hc_start_ms": (as_timestamp(ns.suivant) * 1000) if ns.suivant != '' else none, "next_hc_start_ms": (as_timestamp(ns.suivant) * 1000) if ns.suivant != '' else none,
"next_night_hc_start_ms": (as_timestamp(ns.nuit) * 1000) if ns.nuit != '' else none, "next_night_hc_start_ms": (as_timestamp(ns.nuit) * 1000) if ns.nuit != '' else none,
"night_hc_start_ms": (as_timestamp(ns.nd) * 1000) if ns.nd != '' else none,
"night_hc_end_ms": (as_timestamp(ns.nf) * 1000) if ns.nf != '' else none,
"nowMs": as_timestamp(now()) * 1000, "nowMs": as_timestamp(now()) * 1000,
"balance_enabled": states("input_boolean.evcs_balance_enabled"), "balance_enabled": states("input_boolean.evcs_balance_enabled"),

View File

@@ -77,6 +77,10 @@ return [{
hc_window_end_ms: snapshot.hc_window_end_ms, hc_window_end_ms: snapshot.hc_window_end_ms,
next_hc_start_ms: snapshot.next_hc_start_ms, next_hc_start_ms: snapshot.next_hc_start_ms,
next_night_hc_start_ms: snapshot.next_night_hc_start_ms, next_night_hc_start_ms: snapshot.next_night_hc_start_ms,
// Fenetre HC de nuit en cours ou prochaine (29/09) : import prevu si la
// session de nuit s'engage.
night_hc_start_ms: snapshot.night_hc_start_ms,
night_hc_end_ms: snapshot.night_hc_end_ms,
// Repris pour l'écriture des helpers, sans influencer le calcul. // Repris pour l'écriture des helpers, sans influencer le calcul.
resourceUsable: resource.usable, resourceUsable: resource.usable,

View File

@@ -336,7 +336,8 @@ for (const [nom, r, test] of imp) {
const t = new Date('2026-09-27T23:30:00Z'); // 01:30 a Paris const t = new Date('2026-09-27T23:30:00Z'); // 01:30 a Paris
const pn = [ const pn = [
['nuit 28/09 : 5,77 kWh EDF, 5,47 a couvrir, retour 09 h, aucune voiture', previsionNuit(base(), t), ['nuit 28/09 : 5,77 kWh EDF, 5,47 a couvrir, retour 09 h, aucune voiture', previsionNuit(base(), t),
(r) => r.etat === 5.77 && r.attributs.voiture === 'aucune' && /5.77 kWh a prendre sur EDF/.test(r.attributs.explication)], (r) => r.etat === 5.77 && r.attributs.voiture === 'aucune' && /au moins 5,77 kWh achetes/.test(r.attributs.explication)
&& /plus la consommation de la maison/.test(r.attributs.explication)],
['jamais 73,7 ni 26,95 dans la prevision (incident 27/09)', previsionNuit(base(), t), ['jamais 73,7 ni 26,95 dans la prevision (incident 27/09)', previsionNuit(base(), t),
(r) => !/73\.7|26\.95/.test(JSON.stringify(r))], (r) => !/73\.7|26\.95/.test(JSON.stringify(r))],
['bilan perime (> 30 min) -> indisponible', previsionNuit(base(), new Date('2026-09-28T00:30:00Z')), ['bilan perime (> 30 min) -> indisponible', previsionNuit(base(), new Date('2026-09-28T00:30:00Z')),
@@ -370,6 +371,34 @@ for (const [nom, r, test] of imp) {
'sensor.victron_battery_soc': '70', 'input_boolean.victron_evcs_night_grid_session_committed': 'off', 'sensor.victron_battery_soc': '70', 'input_boolean.victron_evcs_night_grid_session_committed': 'off',
})) m.set(k, { state: v }); return previsionNuit(m, t); })(), })) m.set(k, { state: v }); return previsionNuit(m, t); })(),
(r) => r.attributs.verdict === 'juste'], (r) => r.attributs.verdict === 'juste'],
// Releve REEL du 28/09 21:32 : verdict reseau_necessaire, manque 0,14 kWh.
// L'agent a dit « environ 0,14 kWh a prendre sur EDF » ; le reseau en a
// fourni 4,6 (la maison sur le reseau de 01:24 a 07:24).
['28/09 21:32 : manque 0,14 mais la maison passe la nuit sur le reseau -> « environ 4,6 kWh achetes »', (() => {
const m = base(); for (const [k, v] of Object.entries({
'input_number.evcs_balance_grid_energy_needed_kwh': '0.14', 'input_boolean.victron_evcs_night_grid_session_committed': 'off',
'input_number.evcs_balance_night_hc_house_kwh': '4.5', 'sensor.evcs_reseau': 'non_raccorde',
'input_datetime.victron_evcs_hc_night_start': '01:24:00', 'input_datetime.victron_evcs_hc_night_stop': '07:24:00',
})) m.set(k, { state: v }); return previsionNuit(m, t); })(),
(r) => r.etat === 4.6 && r.attributs.verdict === 'reseau_necessaire' && r.attributs.import_prevu_kwh === 4.6
&& r.attributs.manque_batterie_kwh === 0.14
&& /EDF sera branche de 01:24 a 07:24 : environ 4,6 kWh achetes/.test(r.attributs.explication)
&& /maison 4,5, batterie 0,14/.test(r.attributs.explication)
&& !/0,14 kWh a prendre/.test(r.attributs.explication)],
['session engagee avec la maison connue -> achat = maison + batterie, jusqu a 07:24', (() => {
const m = base(); for (const [k, v] of Object.entries({
'input_number.evcs_balance_night_hc_house_kwh': '3.2', 'input_datetime.victron_evcs_hc_night_stop': '07:24:00',
})) m.set(k, { state: v }); return previsionNuit(m, t); })(),
(r) => r.etat === 9 && /jusqu a 07:24 : environ 9 kWh achetes/.test(r.attributs.explication)],
['« c est juste » annonce aussi l achat si EDF s engage', (() => {
const m = base(); for (const [k, v] of Object.entries({
'input_boolean.evcs_balance_grid_needed': 'off', 'input_number.evcs_balance_required_soc': '57.5',
'input_number.evcs_balance_required_soc_prudent': '68.5', 'sensor.victron_battery_soc': '58',
'input_boolean.victron_evcs_night_grid_session_committed': 'off', 'input_number.evcs_balance_night_hc_house_kwh': '4.46',
})) m.set(k, { state: v }); return previsionNuit(m, t); })(),
(r) => r.etat === 0 && r.attributs.verdict === 'juste' && /la maison y achetera alors environ 4,5 kWh/.test(r.attributs.explication)],
['plus jamais d attribut reseau_necessaire_kwh (lu comme un achat le 28/09)', previsionNuit(base(), t),
(r) => !('reseau_necessaire_kwh' in r.attributs) && 'import_prevu_kwh' in r.attributs],
['jamais d attribut qui ressemble a un SOC du matin (incident 21:16)', previsionNuit(base(), t), ['jamais d attribut qui ressemble a un SOC du matin (incident 21:16)', previsionNuit(base(), t),
(r) => !('soc_requis_au_retour_solaire_pct' in r.attributs) && 'soc_requis_maintenant_pct' in r.attributs], (r) => !('soc_requis_au_retour_solaire_pct' in r.attributs) && 'soc_requis_maintenant_pct' in r.attributs],
]; ];

View File

@@ -109,6 +109,9 @@ const SUMMARIZE = {
// Nuit prevue par la soiree (28/09). // Nuit prevue par la soiree (28/09).
nightModelActive: o.nightModel ? o.nightModel.active : false, nightModelActive: o.nightModel ? o.nightModel.active : false,
nightFactor: o.nightModel ? o.nightModel.factor : null, nightFactor: o.nightModel ? o.nightModel.factor : null,
// Maison sur le reseau pendant la fenetre HC de nuit (29/09).
nightHcActive: o.nightHcImport ? o.nightHcImport.active : false,
nightHcHouseKwh: o.nightHcImport ? o.nightHcImport.houseKwh : null,
// Voiture branchee en HC, depart avant la prochaine HC (28/09). // Voiture branchee en HC, depart avant la prochaine HC (28/09).
vehicleActive: o.vehicleMidday ? o.vehicleMidday.active : false, vehicleActive: o.vehicleMidday ? o.vehicleMidday.active : false,
vehicleNeedKwh: o.vehicleMidday ? o.vehicleMidday.needKwh : null, vehicleNeedKwh: o.vehicleMidday ? o.vehicleMidday.needKwh : null,

View File

@@ -557,4 +557,47 @@ export const balanceScenarios = [
night_house_computed_ms: Date.parse('2026-09-27T22:57: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 }, expect: { usable: true, nightModelActive: true, nightFactor: 1.45, requiredSocPct: 59.8, prudentRequiredSocPct: 62.2 },
}, },
// ======== CE QUE LA SESSION DE NUIT ACHETERA (29/09/2026) ========
// Nuit du 28-29/09 : « 0,14 kWh a prendre sur EDF » annonces, 4,6 importes :
// la session engagee fait passer TOUTE la maison sur le reseau de 01:24 a
// 07:24. Verifie a la main sur le profil p50 (pas de soleil avant 8 h) :
// 0,6 x 0,80 + 0,70 + 0,60 + 0,70 + 0,70 + 0,70 + 0,4 x 0,70 = 4,16 kWh.
{
node: 'BALANCE',
id: 'bilan-hc-nuit-maison-sur-le-reseau',
label: '23:00, fenetre HC de nuit 01:24-07:24 a venir : la maison y prendra 4,16 kWh au reseau',
snapshot: Object.assign(snap2709('2026-09-27T23:00:00+02:00', 52, null), {
night_hc_start_ms: Date.parse('2026-09-28T01:24:00+02:00'), night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00') }),
expect: { usable: true, nightHcActive: true, nightHcHouseKwh: 4.16, requiredSocPct: 47.9 },
},
{
node: 'BALANCE',
id: 'bilan-hc-nuit-maison-recalee-sur-la-soiree',
label: 'Soiree chargee (facteur 1,092) : la maison en HC de nuit monte a 4,53 kWh',
// Tranches recalees puis arrondies : 0,87 / 0,76 / 0,66 -> 0,6 x 0,87 +
// 0,76 + 0,66 + 3 x 0,76 + 0,4 x 0,76 = 4,53.
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-27T22:57:00+02:00'),
night_hc_start_ms: Date.parse('2026-09-28T01:24:00+02:00'), night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00') }),
expect: { usable: true, nightModelActive: true, nightHcActive: true, nightHcHouseKwh: 4.53 },
},
{
node: 'BALANCE',
id: 'bilan-hc-nuit-fenetre-ouverte-depuis-maintenant',
label: '03:30, fenetre ouverte : seul le reste de la fenetre compte (2,68 kWh)',
// La tranche de 03 h n'est comptee que pour sa demi-heure restante :
// 0,5 x 0,60 + 0,70 + 0,70 + 0,70 + 0,4 x 0,70 = 2,68.
snapshot: Object.assign(snap2709('2026-09-28T03:30:00+02:00', 45, null), {
night_hc_start_ms: Date.parse('2026-09-28T01:24:00+02:00'), night_hc_end_ms: Date.parse('2026-09-28T07:24:00+02:00') }),
expect: { usable: true, nightHcActive: true, nightHcHouseKwh: 2.68 },
},
{
node: 'BALANCE',
id: 'bilan-hc-nuit-fenetre-inconnue',
label: 'Fenetre HC de nuit non transmise : rien n est annonce',
snapshot: snap2709('2026-09-27T23:00:00+02:00', 52, null),
expect: { usable: true, nightHcActive: false, nightHcHouseKwh: null },
},
]; ];

View File

@@ -593,6 +593,16 @@ export function previsionNuit(st, maintenant = new Date()) {
const session = txt('input_boolean.victron_evcs_night_grid_session_committed') === 'on'; const session = txt('input_boolean.victron_evcs_night_grid_session_committed') === 'on';
const branche = txt('binary_sensor.victron_evcs_vehicle_connected'); const branche = txt('binary_sensor.victron_evcs_vehicle_connected');
const hcDebut = (txt('input_datetime.victron_evcs_hc_night_start') || '').slice(0, 5) || null; const hcDebut = (txt('input_datetime.victron_evcs_hc_night_start') || '').slice(0, 5) || null;
const hcFin = (txt('input_datetime.victron_evcs_hc_night_stop') || '').slice(0, 5) || null;
// CE QUI SERA ACHETE, pas le manque de la batterie (incident 28/09 21:32 :
// « 0,14 kWh a prendre sur EDF » annonces, 4,6 importes). Une session engagee
// tient la batterie et fait passer TOUTE la maison sur le reseau jusqu'a la
// fin des heures creuses : achat = maison sur la fenetre (calculee par le
// bilan) + manque de la batterie. 0 publie = fenetre inconnue -> null.
const maisonHc = nombre(st, 'input_number.evcs_balance_night_hc_house_kwh');
const maisonHcKwh = maisonHc !== null && maisonHc > 0 ? maisonHc : null;
const manqueKwh = reseauNecessaire && reseauKwh !== null ? reseauKwh : 0;
const importPrevu = maisonHcKwh !== null ? Math.round((maisonHcKwh + manqueKwh) * 10) / 10 : null;
const marge = soc !== null && socRequis !== null ? Math.round((soc - socRequis) * 10) / 10 : null; const marge = soc !== null && socRequis !== null ? Math.round((soc - socRequis) * 10) / 10 : null;
// TROIS verdicts, pas deux (incident 28/09 21:16 : « la batterie suffit, sans // TROIS verdicts, pas deux (incident 28/09 21:16 : « la batterie suffit, sans
@@ -607,10 +617,13 @@ export function previsionNuit(st, maintenant = new Date()) {
const attributs = { const attributs = {
maison_kwh_a_couvrir: aCouvrir, heure_retour_solaire: retour, maison_kwh_a_couvrir: aCouvrir, heure_retour_solaire: retour,
reseau_necessaire: reseauNecessaire, reseau_necessaire_kwh: reseauNecessaire ? reseauKwh : 0, reseau_necessaire: reseauNecessaire,
// ⚠️ Plus de `reseau_necessaire_kwh` (29/09) : c'etait le manque de la
// batterie, lu comme ce qui serait achete. Deux grandeurs, deux noms.
import_prevu_kwh: importPrevu, maison_en_heures_creuses_kwh: maisonHcKwh, manque_batterie_kwh: manqueKwh,
verdict, verdict,
soc_actuel_pct: soc, soc_requis_maintenant_pct: socRequis, soc_requis_prudent_maintenant_pct: socPrudent, soc_actuel_pct: soc, soc_requis_maintenant_pct: socRequis, soc_requis_prudent_maintenant_pct: socPrudent,
marge_points: marge, debut_heures_creuses: hcDebut, marge_points: marge, debut_heures_creuses: hcDebut, fin_heures_creuses: hcFin,
contacteur_edf: reseau, session_reseau_nuit_engagee: session, contacteur_edf: reseau, session_reseau_nuit_engagee: session,
voiture: branche === 'on' ? 'branchee' : branche === 'off' ? 'aucune' : 'inconnue', voiture: branche === 'on' ? 'branchee' : branche === 'off' ? 'aucune' : 'inconnue',
bilan_calcule_il_y_a_min: age, bilan_calcule_il_y_a_min: age,
@@ -629,16 +642,26 @@ export function previsionNuit(st, maintenant = new Date()) {
? 'Un vehicule est branche : sa charge s ajoute et sera decidee par le controleur.' ? 'Un vehicule est branche : sa charge s ajoute et sera decidee par le controleur.'
: 'Etat du vehicule inconnu.'; : 'Etat du vehicule inconnu.';
const hc = hcDebut ? `a ${hcDebut}` : 'a l ouverture des heures creuses'; const hc = hcDebut ? `a ${hcDebut}` : 'a l ouverture des heures creuses';
const kwh = (x) => String(x).replace('.', ',');
const jusqua = hcFin ? `jusqu a ${hcFin}` : 'jusqu a la fin des heures creuses';
const plage = hcDebut && hcFin ? `de ${hcDebut} a ${hcFin}` : 'en heures creuses';
const detail = maisonHcKwh === null ? ''
: manqueKwh > 0 ? ` (maison ${kwh(maisonHcKwh)}, batterie ${kwh(manqueKwh)})` : '';
const achat = importPrevu !== null
? `environ ${kwh(importPrevu)} kWh achetes en heures creuses${detail}, car la maison passe la nuit sur le reseau`
: manqueKwh > 0 ? `au moins ${kwh(manqueKwh)} kWh achetes en heures creuses, plus la consommation de la maison` : null;
const edf = { const edf = {
edf_engage: `EDF est branche pour la nuit, en heures creuses${reseauNecessaire ? ` : environ ${reseauKwh} kWh a prendre sur EDF avant le retour du solaire` : ''} ; la batterie est tenue jusqu au matin.`, edf_engage: `EDF est branche pour la nuit, ${jusqua}${achat ? ` : ${achat}` : ''} ; la batterie est tenue jusqu au matin.`,
reseau_necessaire: `Environ ${reseauKwh} kWh a prendre sur EDF en heures creuses avant le retour du solaire.`, reseau_necessaire: `EDF sera branche ${plage}${achat ? ` : ${achat}` : ''}.`,
batterie_suffit: 'La batterie suffit jusqu au retour du solaire, meme si la maison consomme plus que d habitude : EDF ne devrait pas etre necessaire.', batterie_suffit: 'La batterie suffit jusqu au retour du solaire, meme si la maison consomme plus que d habitude : EDF ne devrait pas etre necessaire.',
juste: `C est juste : la batterie suffit seulement si la maison consomme comme d habitude${marge !== null ? ` (marge ${String(marge).replace('.', ',')} point${Math.abs(marge) >= 2 ? 's' : ''})` : ''}. ` + juste: `C est juste : la batterie suffit seulement si la maison consomme comme d habitude${marge !== null ? ` (marge ${kwh(marge)} point${Math.abs(marge) >= 2 ? 's' : ''})` : ''}. ` +
`Le systeme refait le calcul ${hc} : si la soiree a consomme davantage, il prendra EDF en heures creuses.`, `Le systeme refait le calcul ${hc} : si la soiree a consomme davantage, il prendra EDF en heures creuses` +
`${maisonHcKwh !== null ? `, et la maison y achetera alors environ ${kwh(Math.round(maisonHcKwh * 10) / 10)} kWh` : ''}.`,
}[verdict]; }[verdict];
const texte = `La maison doit couvrir environ ${aCouvrir} kWh jusqu au retour du solaire vers ${retour}. ` + const texte = `La maison doit couvrir environ ${aCouvrir} kWh jusqu au retour du solaire vers ${retour}. ` +
`${edf} Batterie a ${soc !== null ? Math.round(soc) : '?'} %. ${voiture}`; `${edf} Batterie a ${soc !== null ? Math.round(soc) : '?'} %. ${voiture}`;
return { etat: reseauNecessaire ? reseauKwh : 0, attributs: { ...attributs, statut: 'frais', explication: texte, texte_rapport: texte } }; const etat = verdict === 'edf_engage' || verdict === 'reseau_necessaire' ? (importPrevu ?? manqueKwh) : 0;
return { etat, attributs: { ...attributs, statut: 'frais', explication: texte, texte_rapport: texte } };
} }
// --------------------------------------------------------------------- // ---------------------------------------------------------------------

File diff suppressed because one or more lines are too long