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:
353
src/water-heater/ARBITRATION.js
Normal file
353
src/water-heater/ARBITRATION.js
Normal 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
|
||||
}
|
||||
}
|
||||
}];
|
||||
Reference in New Issue
Block a user