Chauffe-eau en HC de nuit les jours faibles : decision quotidienne du bilan, workflow ECS sous controle du depot (VRM, prevision du jour), energie 24 h

This commit is contained in:
Tallagrand Thierry
2026-10-07 07:06:03 +02:00
parent 0e0c0a558d
commit c2824085c9
26 changed files with 3060 additions and 42 deletions

View File

@@ -0,0 +1,353 @@
function regimeRank(regime) {
switch (regime) {
case 'off': return 0;
case 'hp_low': return 1;
case 'resistive_mid': return 2;
case 'boost_high': return 3;
default: return -1;
}
}
function allowedRankForMode(mode) {
switch (mode) {
case 'off': return 0;
case 'green': return 1;
case 'auto': return 2;
case 'boost': return 3;
default: return 0;
}
}
function round1(v) {
return Math.round(v * 10) / 10;
}
const root = $input.first()?.json ?? {};
const now = root.now ?? {};
const nowMs = root.nowMs ?? null;
const normalized = root.normalized ?? {};
const observation = root.observation ?? {};
const energyModel = root.energyModel ?? {};
const baseDecision = root.baseDecision ?? {};
let {
recommendedMode,
adviceColor,
state,
reason,
baseRecommendedMode,
baseAdviceColor,
baseState
} = baseDecision;
reason = Array.isArray(reason) ? [...reason] : [];
const {
advisorEnabled,
soc,
batteryCurrentA,
pvPowerAvg10mW,
houseAcPowerW,
todayRemainingKwh,
tomorrowKwh,
baseLoadRemainingKwh,
flexibleSafetyMarginKwh,
acLoadsMaxSafeW,
hotWaterDemand,
gridIsOn,
acInAvailable,
gridAvailable
} = normalized;
const {
evcsPriorityActive,
energyPriorityReason,
waterHeaterSwitchOn,
observedPowerW,
observedRegime,
mismatchSinceAt,
mismatchSinceMsPrev,
forcedOffByAdvisorPrev,
forcedOffReasonPrev,
forcedOffAtPrev,
restoreSurplusW,
modeMismatchDelayMin,
sampleDurationMin,
energyDeltaKwh,
sampleIsUsable
} = observation;
const {
targetSoc,
energyNeededToReachTargetKwh,
greenEnergyKwh,
autoEnergyKwh,
boostEnergyKwh,
projectedGreenSoc,
projectedAutoSoc,
projectedBoostSoc,
greenBudgetMarginKwh,
autoBudgetMarginKwh,
boostBudgetMarginKwh,
forecastStrength,
netPvHeadroomW,
pvCoversHouseNow,
pvStartedWell,
greenAllowed,
autoAllowed,
boostAllowed,
batteryNotDrainingHard
} = energyModel;
if (advisorEnabled && evcsPriorityActive) {
if (recommendedMode === 'boost' || recommendedMode === 'auto') {
if (greenAllowed && batteryNotDrainingHard) {
recommendedMode = 'green';
adviceColor = 'orange';
state = 'evcs_priority_green_cap';
reason.push(`Priorité EVCS active : chauffe-eau plafonné à GREEN (${energyPriorityReason || 'raison non précisée'})`);
} else {
recommendedMode = 'off';
adviceColor = 'red';
state = 'evcs_priority_off_cap';
reason.push(`Priorité EVCS active : chauffe-eau déconseillé (${energyPriorityReason || 'raison non précisée'})`);
}
} else if (recommendedMode === 'green' && (!greenAllowed || !batteryNotDrainingHard)) {
recommendedMode = 'off';
adviceColor = 'red';
state = 'evcs_priority_green_blocked';
reason.push(`Priorité EVCS active : GREEN devient lui-même trop coûteux (${energyPriorityReason || 'raison non précisée'})`);
}
}
const selectedLoadKwh =
recommendedMode === 'green' ? greenEnergyKwh :
recommendedMode === 'auto' ? autoEnergyKwh :
recommendedMode === 'boost' ? boostEnergyKwh :
0;
const selectedProjectedSoc =
recommendedMode === 'green' ? projectedGreenSoc :
recommendedMode === 'auto' ? projectedAutoSoc :
recommendedMode === 'boost' ? projectedBoostSoc :
soc;
const selectedBudgetMarginKwh =
recommendedMode === 'green' ? greenBudgetMarginKwh :
recommendedMode === 'auto' ? autoBudgetMarginKwh :
recommendedMode === 'boost' ? boostBudgetMarginKwh :
todayRemainingKwh - baseLoadRemainingKwh - flexibleSafetyMarginKwh - energyNeededToReachTargetKwh;
const legacyAdviceOk = recommendedMode !== 'off';
const observedRank = regimeRank(observedRegime);
const allowedRank = allowedRankForMode(recommendedMode);
const mismatchActive =
waterHeaterSwitchOn &&
observedRank >= 0 &&
allowedRank >= 0 &&
observedRank > allowedRank;
let mismatchSeverity = 'none';
if (mismatchActive) {
if (recommendedMode === 'off') {
mismatchSeverity = 'critical';
} else if (observedRank - allowedRank >= 2) {
mismatchSeverity = 'high';
} else {
mismatchSeverity = 'medium';
}
}
const mismatchSinceMsEffective = mismatchActive ? (mismatchSinceMsPrev ?? nowMs) : null;
const mismatchSinceAtEffective = mismatchActive ? (mismatchSinceMsPrev ? mismatchSinceAt : now.stamp) : '';
const mismatchDurationMin = mismatchActive && mismatchSinceMsEffective !== null ? Math.max(0, (nowMs - mismatchSinceMsEffective) / 60000) : 0;
let pendingUserAction = '';
if (mismatchActive) {
if (recommendedMode === 'off') {
pendingUserAction = evcsPriorityActive
? 'Priorité EVCS : coupe temporairement le chauffe-eau ou laisse-le hors tension'
: 'Le chauffe-eau consomme alors qu\'il est déconseillé : coupe-le ou attends une meilleure fenêtre';
} else if (recommendedMode === 'green') {
pendingUserAction = observedRegime === 'boost_high'
? 'Passe le chauffe-eau en GREEN si possible, il semble en régime BOOST/résistance forte'
: 'Passe le chauffe-eau en GREEN si possible, il semble plus gourmand que prévu';
} else if (recommendedMode === 'auto') {
pendingUserAction = 'Le chauffe-eau semble trop gourmand pour AUTO : repasse-le en AUTO modéré ou GREEN si possible';
}
}
const firstMismatchRun = mismatchActive && (mismatchSinceMsPrev === null || mismatchDurationMin < 1);
const realConflictNow =
evcsPriorityActive &&
(
batteryCurrentA < -1 ||
pvPowerAvg10mW < restoreSurplusW ||
houseAcPowerW > acLoadsMaxSafeW ||
recommendedMode === 'off' ||
observedRegime === 'boost_high' ||
(recommendedMode === 'green' && observedRegime === 'resistive_mid')
);
const forceOffNow =
advisorEnabled &&
waterHeaterSwitchOn &&
!forcedOffByAdvisorPrev &&
mismatchActive &&
mismatchDurationMin >= modeMismatchDelayMin &&
realConflictNow;
const restoreEnergyOk =
pvPowerAvg10mW >= restoreSurplusW &&
batteryCurrentA >= -1 &&
houseAcPowerW <= acLoadsMaxSafeW;
const restoreNow =
advisorEnabled &&
forcedOffByAdvisorPrev &&
!waterHeaterSwitchOn &&
(
(!evcsPriorityActive && recommendedMode !== 'off') ||
restoreEnergyOk
);
let forcedOffByAdvisor = forcedOffByAdvisorPrev;
let forcedOffReason = forcedOffReasonPrev;
let forcedOffAt = forcedOffAtPrev;
let protectionState = 'none';
let powerSwitchAction = 'none';
let powerSwitchDesiredOn = waterHeaterSwitchOn;
if (forceOffNow) {
powerSwitchAction = 'turn_off';
powerSwitchDesiredOn = false;
forcedOffByAdvisor = true;
forcedOffReason = `Coupure temporaire ECS: priorité EVCS + régime ${observedRegime} trop gourmand`;
forcedOffAt = now.stamp;
protectionState = 'forced_off_now_evcs_priority';
state = 'forced_off_now_evcs_priority';
adviceColor = 'red';
reason.push(forcedOffReason);
}
else if (restoreNow) {
powerSwitchAction = 'turn_on';
powerSwitchDesiredOn = true;
forcedOffByAdvisor = false;
forcedOffReason = '';
protectionState = 'restored_after_evcs_priority';
state = 'restored_after_evcs_priority';
reason.push('Remise sous tension ECS : conflit EVCS terminé ou surplus redevenu suffisant');
}
else if (forcedOffByAdvisorPrev && !waterHeaterSwitchOn) {
protectionState = 'forced_off_wait_restore';
state = 'forced_off_wait_restore';
reason.push(`ECS temporairement coupé par l'advisor (${forcedOffReasonPrev || 'raison non précisée'})`);
}
else if (mismatchActive && evcsPriorityActive) {
protectionState = 'watch_mismatch_evcs_priority';
state = 'watch_mismatch_evcs_priority';
reason.push(`Écart de régime ECS sous priorité EVCS depuis ${round1(mismatchDurationMin)} min`);
}
if (!forceOffNow && !restoreNow && forcedOffByAdvisorPrev && waterHeaterSwitchOn) {
forcedOffByAdvisor = false;
forcedOffReason = '';
}
// =====================================================
// JOUR FAIBLE : EAU CHAUDE EN HC DE NUIT (07/10/2026, option b du proprietaire)
// =====================================================
// Le bilan decide une fois par jour (input_boolean.evcs_balance_ecs_nuit) : si
// le solaire prevu ne remplit pas la batterie pour la nuit, EDF sera ferme de
// toute facon -> le chauffe-eau est COUPE le jour et chauffe pendant une
// session reseau HC engagee (contacteur ferme), au tarif creux, jamais sur la
// batterie. Securite eau chaude : moins de ECS_EAU_FROIDE_KWH en 24 h -> il
// chauffe quand meme. Energie 24 h illisible -> aucune restriction (le
// comportement d'avant revient, jamais un ballon froid par panne de capteur).
// Jour normal : le chauffe-eau coupe par cette regle est remis sous tension.
const ECS_EAU_FROIDE_KWH = 1.5;
const {
ecsNuit = false, hcSessionCommitted = false, ecs24hKwh = NaN, offJourFaiblePrev = false,
} = normalized;
let offJourFaible = false;
let jourFaibleState = 'none';
if (advisorEnabled && ecsNuit) {
const energieConnue = Number.isFinite(ecs24hKwh);
const eauFroide = energieConnue && ecs24hKwh < ECS_EAU_FROIDE_KWH;
const heuresCreusesEdf = hcSessionCommitted && gridIsOn;
const veutChauffer = !energieConnue || heuresCreusesEdf || eauFroide;
offJourFaible = true;
jourFaibleState = !energieConnue ? 'jour_faible_energie_illisible'
: heuresCreusesEdf ? 'jour_faible_chauffe_hc_edf'
: eauFroide ? 'jour_faible_eau_froide'
: 'jour_faible_coupe';
if (veutChauffer !== waterHeaterSwitchOn) {
powerSwitchAction = veutChauffer ? 'turn_on' : 'turn_off';
powerSwitchDesiredOn = veutChauffer;
} else {
powerSwitchAction = 'none';
powerSwitchDesiredOn = waterHeaterSwitchOn;
}
forcedOffByAdvisor = false;
forcedOffReason = '';
protectionState = jourFaibleState;
state = jourFaibleState;
reason.push(
jourFaibleState === 'jour_faible_chauffe_hc_edf' ? 'Jour faible : eau chaude en heures creuses sur EDF (session reseau engagee)'
: jourFaibleState === 'jour_faible_eau_froide' ? `Jour faible mais seulement ${round1(ecs24hKwh)} kWh en 24 h : chauffe pour garantir l eau chaude`
: jourFaibleState === 'jour_faible_energie_illisible' ? 'Jour faible, energie 24 h illisible : aucune restriction'
: 'Jour faible : chauffe-eau coupe, il chauffera en heures creuses sur EDF'
);
} else if (advisorEnabled && offJourFaiblePrev && !waterHeaterSwitchOn) {
powerSwitchAction = 'turn_on';
powerSwitchDesiredOn = true;
jourFaibleState = 'jour_normal_remise_sous_tension';
protectionState = jourFaibleState;
state = jourFaibleState;
reason.push('Jour normal : chauffe-eau remis sous tension (il chauffe au solaire)');
}
return [{
json: {
...root,
meta: {
...(root.meta ?? {}),
node: 'ecs_final_arbitration_and_protection'
},
finalDecision: {
recommendedMode,
adviceColor,
state,
reason,
baseRecommendedMode,
baseAdviceColor,
baseState,
selectedLoadKwh,
selectedProjectedSoc,
selectedBudgetMarginKwh,
legacyAdviceOk,
mismatchActive,
mismatchSeverity,
mismatchSinceAtEffective,
mismatchSinceMsEffective,
mismatchDurationMin,
pendingUserAction,
firstMismatchRun,
realConflictNow,
forceOffNow,
restoreEnergyOk,
restoreNow,
forcedOffByAdvisor,
forcedOffReason,
forcedOffAt,
protectionState,
powerSwitchAction,
powerSwitchDesiredOn,
offJourFaible,
jourFaibleState
}
}
}];