Chauffe-eau : reprise de 16 h a 20 h apres coupure en pleine chauffe, mesure sur 10 min ; bilan HC de midi

This commit is contained in:
Tallagrand Thierry
2026-10-07 14:40:21 +02:00
parent b34b68408c
commit 16d1f8ee2b
8 changed files with 101 additions and 15 deletions

View File

@@ -2063,5 +2063,22 @@ Deux freins empilaient leurs effets a midi (SOC 31 -> 35 % pour ~44 % requis) :
⚠️ A verifier : la prochaine session HC avec manque (cette nuit, jour faible) : courant ACM calcule, consigne,
SOC atteint a 07:24 contre la cible. La fenetre de midi du 07/10 s'est fermee 2 min apres le deploiement.
### 07/10 14:31 — Bilan de la HC de midi, et deux corrections du chauffe-eau
- **Bilan** : EDF 12:24-14:24, ballon 12:25-14:25 ; **2,30 kWh** chauffes (compteur 19,19 -> 21,49), ~1,15 kW
en moyenne en BOOST ; batterie 30 -> 35 % (14:08), 34 % a 14:27 (~1,4 kWh, corrections arrivees a 14:17 et
14:22) ; EDF ~10 kWh sur la fenetre ; maison 6,3 kW a 13 h (dont ~5 kW hors chauffe-eau) ; production
6,2 kWh a 14:31. Alerte : `active:false` toute la fenetre (BOOST pose a 12:35, `max_w` 2 830) — correct.
- **Coupe en pleine chauffe** : a 14:24 le ballon tirait encore 2,3 kW (eau pas a 65 °C), et la securite du soir
(< 1,5 kWh depuis 07:24) ne pouvait plus jouer (2,7 kWh). **Correctif (accord, deploye 14:37)** : quand la
regle jour faible coupe le ballon alors qu'il chauffe (puissance mesuree ou precedente > 150 W), elle ecrit
`input_text.victron_water_heater_coupe_en_chauffe` (« AAAA-MM-JJ HH:MM ») ; ce jour-la, la securite de
16 h-20 h joue meme au-dela de 1,5 kWh. Valeur posee a la main pour le 07/10 (« 2026-10-07 14:25 »). La
reprise peut se faire sur la batterie : arbitrage confort assume.
- **Mesure sur 10 min** : le compteur Tuya ne remonte que toutes les ~10 min ; mesure sur 5 min = 2,3 kW / 0 W
en alternance (regime qui clignote). `sampleWindowMinMin` 2 -> 9,5 (max 15 -> 25) ; la reference
(`energy_total_prev_kwh` / `_prev_at`) n'avance que si l'echantillon a servi ou est perime.
- Tests ECS 23/23 (horloge simulee dans le harnais pour la reprise du soir ; coupure de la veille ignoree),
mutation qui mord.
---
*Document mis a jour le : 07/10/2026 (~14:25) — recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*
*Document mis a jour le : 07/10/2026 (~14:40) — bilan HC de midi, reprise du soir apres coupure en pleine chauffe, mesure ECS sur 10 min ; recharge reseau HC : courant calcule (12 kVA) et consigne non tenue ; HC de midi : AUTO ne chauffe pas (BOOST), alerte v2, Jeep (contact, relecture, zone), recharge batterie bridee a 30 A par l'ACM ; jour faible = EDF les deux fenetres HC pour le chauffe-eau, alerte BOOST, Victron Mobile refait ; 1re decision jour faible en reel, mesure ECS reparee, douches 06:00/20:00, Grafana chauffe-eau, tableaux ECS ; chauffe-eau en HC de nuit les jours faibles (decision du bilan, workflow ECS sous controle du depot, VRM, V2.3 desactive) ; garde batterie reseau : voiture bridee a 6 A au lieu d'un arret ; garde-fou concierge 2 h ; allers-retours brefs de la borne retrouves dans l'historique HA (echange VW -> JEEP de 20 s), production 14,5 kWh contre 22 prevus, defaut garde batterie en mode reseau a trancher ; voiture a midi : la batterie n'est plus comptee comme du solaire (EDF automatique les jours de depart si le surplus ne couvre pas) ; nuit 05->06 : JEEP chargee sur EDF en complete (6,32 kWh), budget 0 du a la prevision, Windy/ECMWF contre VRM, a comparer ce soir ; concierge aligne sur la preuve fin de trajet ; a verifier : nuit 05->06 et mardi faible, comparaison VRM/Forecast.Solar ; JEEP identifiee aussi par sa fin de trajet, prevision de la nuit 05->06 ; identification JEEP reussie en reel, correction humaine qui prime (refus supprime), session du 04/10 coupee ; EVCS Master en quatre onglets ; Jeep lue par Uconnect et identifiee seule, capacite JEEP 10 kWh, concierge variante (a) ; releve par photo (SPARK), hass-uconnect en attente ; ecart semaine/week-end mesure (pas de profil week-end la nuit) ; nuit du budget : budget borne au plancher prudent, sans EDF = voiture bloquee meme EDF ferme (batterie seule) ; Energie : sauts corriges ; tableau Energie complete (Sud/Ouest, batterie par integration, borne ; aucun redemarrage HA) ; kilometrage des vehicules (km/kWh, capacite, releves de reference, agent cable 15/15) ; a verifier le 04/10 au matin : nuit du budget JEEP ; concierge : vehicule non confirme demande, heure de saisie ; Grafana vehicules en tableau ; Grafana vehicules : ZERO et Invite ajoutes, session 16:46 reetiquetee ZERO ; prevision SOC 10 h ~35 % ; concierge fiable : niveau note par le script, une ligne, dedoublonnage, simulation par drapeau ; agent : message incompris = question, bourse sur demande explicite (banc final 6/6) ; a verifier : nuit du samedi 03 au dimanche 04/10 (budget < besoin JEEP), cron orange-cloture lundi 05/10 apres 17:40, comparaison VRM / Forecast.Solar lundi 05/10*

View File

@@ -18,6 +18,13 @@ input_boolean:
icon: mdi:water-boiler-off
input_text:
# « AAAA-MM-JJ HH:MM » de la derniere coupure faite EN PLEINE CHAUFFE (fin des
# HC) par la regle jour faible : autorise la reprise de 16 h a 20 h ce jour-la
# (douche de 20 h). Ecrit par le workflow ECS (07/10/2026).
victron_water_heater_coupe_en_chauffe:
name: "Chauffe-eau - coupe en pleine chauffe"
max: 32
icon: mdi:water-boiler-alert
# Alerte « passe en BOOST » (07/10/2026) : ecrite par le workflow ECS quand,
# en heures creuses sur EDF, le ballon ne tire que la pompe a chaleur. JSON
# {active, since, fenetre, puissance_w, message} ou chaine vide. Le concierge

View File

@@ -282,6 +282,7 @@ const {
} = normalized;
let offJourFaible = false;
let jourFaibleState = 'none';
let coupeEnChauffe = null; // horodatage a ecrire si la coupure tombe en pleine chauffe
if (advisorEnabled && ecsNuit) {
const energieConnue = Number.isFinite(ecs24hKwh);
const eauFroide = energieConnue && ecs24hKwh < ECS_EAU_FROIDE_KWH;
@@ -292,8 +293,11 @@ if (advisorEnabled && ecsNuit) {
? pvPowerW >= houseAcPowerW
: pvPowerW - houseAcPowerW >= autoPowerW);
const heure = Number.isFinite(now.hour) ? now.hour : -1;
// 07/10 : coupe en PLEINE CHAUFFE a la fin des HC de midi (BOOST encore a 2,3 kW
// a 14:24) -> l'eau n'est pas a 65 °C : il reprend aussi de 16 h a 20 h.
const coupeEnChauffeAujourdhui = (input.coupe_en_chauffe || '').startsWith(now.date || '?');
const securiteSoir = heure >= ECS_SOIR_DEBUT_H && heure < ECS_SOIR_FIN_H
&& !(Number.isFinite(ecsJourKwh) && ecsJourKwh >= ECS_EAU_FROIDE_KWH);
&& (!(Number.isFinite(ecsJourKwh) && ecsJourKwh >= ECS_EAU_FROIDE_KWH) || coupeEnChauffeAujourdhui);
const veutChauffer = !energieConnue || heuresCreusesEdf || eauFroide || solaireCouvre || securiteSoir;
offJourFaible = true;
jourFaibleState = !energieConnue ? 'jour_faible_energie_illisible'
@@ -305,6 +309,10 @@ if (advisorEnabled && ecsNuit) {
if (veutChauffer !== waterHeaterSwitchOn) {
powerSwitchAction = veutChauffer ? 'turn_on' : 'turn_off';
powerSwitchDesiredOn = veutChauffer;
// Coupure alors qu'il chauffait (puissance mesuree au-dessus de l'arret).
if (!veutChauffer && Math.max(observedPowerW, normalized.observedPowerPrevW ?? 0) > 150) {
coupeEnChauffe = `${now.date} ${String(now.hour).padStart(2, '0')}:${String(now.minute).padStart(2, '0')}`;
}
} else {
powerSwitchAction = 'none';
powerSwitchDesiredOn = waterHeaterSwitchOn;
@@ -406,7 +414,8 @@ return [{
powerSwitchDesiredOn,
offJourFaible,
jourFaibleState,
alerteChauffeEau
alerteChauffeEau,
coupeEnChauffe
}
}
}];

View File

@@ -34,6 +34,7 @@
"ecs_jour_kwh": states("sensor.evcs_chauffe_eau_jour_kwh"),
"off_jour_faible_prev": states("input_boolean.victron_water_heater_off_jour_faible"),
"alerte_prev": states("input_text.evcs_alerte_chauffe_eau"),
"coupe_en_chauffe": states("input_text.victron_water_heater_coupe_en_chauffe"),
"hc_midday_start": states("input_datetime.victron_evcs_hc_midday_start"),
"hc_midday_stop": states("input_datetime.victron_evcs_hc_midday_stop"),
"hc_night_start": states("input_datetime.victron_evcs_hc_night_start"),

View File

@@ -141,8 +141,11 @@ const cfg = {
mobileNotifyDomain: 'notify',
mobileNotifyService: 'mobile_app_iphone_16_de_tt',
batteryUsableCapacityKwh: 17.5,
sampleWindowMinMin: 2,
sampleWindowMinMax: 15,
// 07/10/2026 : le compteur Tuya ne remonte que toutes les ~10 min ; mesure sur
// 5 min = une fois 2,3 kW, une fois 0 W (regime qui clignote). On mesure donc
// sur >= 9,5 min : la reference n'est avancee que quand l'echantillon sert.
sampleWindowMinMin: 9.5,
sampleWindowMinMax: 25,
energyByDemandKwh: {
low: { green: 0.9, auto: 1.4, boost: 2.0 },
normal: { green: 1.4, auto: 2.2, boost: 3.1 },
@@ -196,6 +199,7 @@ const ecsNuit = low(input.ecs_nuit) === 'on';
const hcSessionCommitted = low(input.hc_session_committed) === 'on';
const ecs24hKwh = num(input.ecs_24h_kwh, NaN);
const ecsJourKwh = num(input.ecs_jour_kwh, NaN);
const observedPowerPrevW = Math.max(0, num(input.observed_power_helper_w, 0));
const offJourFaiblePrev = low(input.off_jour_faible_prev) === 'on';
const forecastSource = cleanStateText(input.forecast_source) || 'forecast_solar';
// Fenetres HC (« HH:MM:SS » des input_datetime du controleur), en minutes du jour.
@@ -251,6 +255,7 @@ return [{
hcSessionCommitted,
ecs24hKwh,
ecsJourKwh,
observedPowerPrevW,
offJourFaiblePrev,
forecastSource,
hcWindows

View File

@@ -197,7 +197,8 @@ let {
powerSwitchAction,
powerSwitchDesiredOn,
offJourFaible = false,
alerteChauffeEau = ''
alerteChauffeEau = '',
coupeEnChauffe = null
} = finalDecision;
reason = Array.isArray(reason) ? [...reason] : [];
@@ -524,7 +525,10 @@ helper_budget_margin: {
datetime: mismatchSinceAtEffective
}
},
// La reference n'avance que si l'echantillon a servi, ou s'il est perime
// (trop vieux ou illisible) : sinon on laisse s'accumuler jusqu'a 9,5 min.
helper_energy_prev_kwh: {
execute: !!sampleIsUsable || !(Number.isFinite(sampleDurationMin) && sampleDurationMin < 9.5),
domain: 'input_number',
service: 'set_value',
data: {
@@ -533,6 +537,7 @@ helper_budget_margin: {
}
},
helper_energy_prev_at: {
execute: !!sampleIsUsable || !(Number.isFinite(sampleDurationMin) && sampleDurationMin < 9.5),
domain: 'input_datetime',
service: 'set_datetime',
data: {
@@ -564,6 +569,16 @@ helper_budget_margin: {
datetime: forcedOffAtSafe
}
},
// Coupure en pleine chauffe (07/10/2026) : autorise la reprise de 16 h a 20 h.
helper_coupe_en_chauffe: {
execute: !!coupeEnChauffe,
domain: 'input_text',
service: 'set_value',
data: {
entity_id: 'input_text.victron_water_heater_coupe_en_chauffe',
value: coupeEnChauffe || ''
}
},
// Alerte « passe en BOOST » (07/10/2026), lue par le concierge OpenClaw.
helper_alerte_chauffe_eau: {
domain: 'input_text',

View File

@@ -11,10 +11,14 @@ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
const CHAINE = ['NORMALIZE', 'OBSERVATION', 'ENERGY_MODEL', 'BASE_DECISION', 'ARBITRATION', 'OUTPUT_BUILDER', 'BUILD_ACTIONS'];
const code = Object.fromEntries(CHAINE.map((n) => [n, fs.readFileSync(path.join(ROOT, `src/water-heater/${n}.js`), 'utf8')]));
function executer(snapshot) {
function executer(snapshot, fixedMs = null) {
let item = snapshot, sorties = {};
const D = fixedMs === null ? Date : class extends Date {
constructor(...a) { if (a.length === 0) super(fixedMs); else super(...a); }
static now() { return fixedMs; }
};
for (const n of CHAINE) {
const sb = { $input: { first: () => ({ json: item }), all: () => [{ json: item }] }, $env: {}, console };
const sb = { $input: { first: () => ({ json: item }), all: () => [{ json: item }] }, $env: {}, console, Date: D };
vm.createContext(sb);
const out = vm.runInContext(`(function(){ ${code[n]} })()`, sb, { timeout: 5000 });
sorties[n] = out;
@@ -75,7 +79,7 @@ for (const c of cas) {
}
// Coupe = 0 W meme si le compteur remonte en retard (07/10 : « arret — 1595 W »).
{
const stamp = new Date(Date.now() - 5 * 60000).toLocaleString('sv-SE', { timeZone: 'Europe/Paris' });
const stamp = new Date(Date.now() - 10 * 60000).toLocaleString('sv-SE', { timeZone: 'Europe/Paris' });
const s = executer({ ...BASE, water_heater_switch: 'off', water_heater_energy_total_kwh: '18.88', energy_total_prev_kwh: '18.75', energy_prev_at: stamp });
const o = s.OBSERVATION[0].json.observation;
if (o.observedPowerW === 0 && o.observedRegime === 'off') { ok++; console.log('✅ coupe-zero-watt'); }
@@ -85,7 +89,7 @@ for (const c of cas) {
{
const nowMin = (() => { const p = new Intl.DateTimeFormat('fr-FR', { timeZone: 'Europe/Paris', hour: '2-digit', minute: '2-digit', hour12: false }).format(new Date()).split(':'); return Number(p[0]) * 60 + Number(p[1]); })();
const hm = (m) => { const x = ((m % 1440) + 1440) % 1440; return `${String(Math.floor(x / 60)).padStart(2, '0')}:${String(x % 60).padStart(2, '0')}:00`; };
const stamp = new Date(Date.now() - 5 * 60000).toLocaleString('sv-SE', { timeZone: 'Europe/Paris' });
const stamp = new Date(Date.now() - 10 * 60000).toLocaleString('sv-SE', { timeZone: 'Europe/Paris' });
const fenetre = { hc_midday_start: hm(nowMin - 30), hc_midday_stop: hm(nowMin + 60), hc_night_start: hm(nowMin + 300), hc_night_stop: hm(nowMin + 600) };
const base = { ...BASE, ...fenetre, grid_state: 'on', hc_session_committed: 'on', water_heater_switch: 'on',
water_heater_energy_total_kwh: '18.65', energy_total_prev_kwh: '18.60', energy_prev_at: stamp }; // 600 W : pompe a chaleur
@@ -100,12 +104,40 @@ for (const c of cas) {
};
verif('alerte-boost-pompe-a-chaleur-en-hc', {}, true);
verif('alerte-boost-auto-arrete-0-w-en-hc', { water_heater_energy_total_kwh: '18.60' }, true);
verif('alerte-boost-resistance-pas-d-alerte', { water_heater_energy_total_kwh: '18.75' }, false);
verif('alerte-boost-resistance-pas-d-alerte', { water_heater_energy_total_kwh: '18.80' }, false);
verif('alerte-boost-resistance-deja-vue-dans-la-fenetre', { water_heater_energy_total_kwh: '18.60',
alerte_prev: JSON.stringify({ active: false, since: debut, max_w: 1235 }) }, false);
verif('alerte-boost-debut-de-fenetre-attente', { hc_midday_start: hm(nowMin - 5) }, false);
verif('alerte-boost-edf-ouvert-pas-d-alerte', { grid_state: 'off' }, false);
}
// 07/10 : mesure sur >= 9,5 min (compteur Tuya ~10 min) ; reference non avancee sur 5 min.
{
const st5 = new Date(Date.now() - 5 * 60000).toLocaleString('sv-SE', { timeZone: 'Europe/Paris' });
const s = executer({ ...BASE, ecs_nuit: 'off', water_heater_energy_total_kwh: '18.60', energy_total_prev_kwh: '18.60',
energy_prev_at: st5, observed_power_helper_w: '2300', observed_regime_helper: 'boost_high' });
const o = s.OBSERVATION[0].json.observation;
const ref = s.BUILD_ACTIONS.map((x) => x.json).find((c) => c.data?.entity_id === 'input_number.victron_water_heater_energy_total_prev_kwh');
if (o.observedPowerW === 2300 && !ref) { ok++; console.log('✅ mesure-5-min-garde-la-puissance-et-la-reference'); }
else { ko++; console.log(`❌ mesure-5-min-garde-la-puissance-et-la-reference | ${o.observedPowerW} W ref=${!!ref}`); }
}
// 07/10 : coupure en pleine chauffe a la fin des HC -> reprise de 16 h a 20 h meme au-dela de 1,5 kWh.
{
const s1 = executer({ ...BASE, hc_session_committed: 'off', water_heater_switch: 'on', observed_power_helper_w: '2300',
pv_power_w: '1500', house_ac_power_w: '3500' });
const fd1 = s1.ARBITRATION[0].json.finalDecision;
const ecrit = s1.BUILD_ACTIONS.map((x) => x.json).find((c) => c.data?.entity_id === 'input_text.victron_water_heater_coupe_en_chauffe');
if (fd1.powerSwitchAction === 'turn_off' && fd1.coupeEnChauffe && ecrit) { ok++; console.log('✅ coupe-en-pleine-chauffe-memorisee'); }
else { ko++; console.log(`❌ coupe-en-pleine-chauffe-memorisee | ${fd1.powerSwitchAction} ${fd1.coupeEnChauffe} ${!!ecrit}`); }
const soir = Date.parse('2026-10-07T17:00:00+02:00');
const s2 = executer({ ...BASE, water_heater_switch: 'off', off_jour_faible_prev: 'on', ecs_jour_kwh: '2.7', coupe_en_chauffe: '2026-10-07 14:25' }, soir);
const fd2 = s2.ARBITRATION[0].json.finalDecision;
if (fd2.powerSwitchAction === 'turn_on' && fd2.jourFaibleState === 'jour_faible_securite_soir') { ok++; console.log('✅ reprise-du-soir-apres-coupure-en-chauffe'); }
else { ko++; console.log(`❌ reprise-du-soir-apres-coupure-en-chauffe | ${fd2.powerSwitchAction} ${fd2.jourFaibleState}`); }
const s3 = executer({ ...BASE, water_heater_switch: 'off', off_jour_faible_prev: 'on', ecs_jour_kwh: '2.7', coupe_en_chauffe: '2026-10-06 14:25' }, soir);
const fd3 = s3.ARBITRATION[0].json.finalDecision;
if (fd3.powerSwitchAction === 'none' && fd3.jourFaibleState === 'jour_faible_coupe') { ok++; console.log('✅ coupure-en-chauffe-de-la-veille-ignoree'); }
else { ko++; console.log(`❌ coupure-en-chauffe-de-la-veille-ignoree | ${fd3.powerSwitchAction} ${fd3.jourFaibleState}`); }
}
// Prevision forte = le reste du jour seulement (06/10 : demain fort ne rend plus forte une journee faible).
{
const s = executer({ ...BASE, ecs_nuit: 'off', today_remaining_kwh: '8', tomorrow_kwh: '30' });

File diff suppressed because one or more lines are too long