Files
EVCS/decoupesurplus/V2/gridhystP2.js

231 lines
7.2 KiB
JavaScript

// ======================================================
// Timestamps
// ======================================================
// On pose un nouveau "last_on_at" si :
// - on allume réellement EDF
// - ou on adopte un EDF déjà ON avec latch OFF
const setLastOnAt =
gridAction === 'turn_on' ||
state === 'adopt_existing_grid_on' ||
state === 'adopt_existing_grid_on_low_soc';
// On pose "last_off_at" si on coupe réellement EDF
const setLastOffAt =
gridAction === 'turn_off';
// ======================================================
// Context / alerts
// ======================================================
reason.push(`Grid actuel ${gridIsOn ? 'ON' : 'OFF'}`);
reason.push(`Latch actuel ${latchIsOn ? 'ON' : 'OFF'}`);
reason.push(`SOC ${round1(soc)}% | seuils ${gridSocOn}%/${gridSocOff}%`);
reason.push(`PV ${Math.round(pvPowerW)}W`);
reason.push(`Charge AC maison ${Math.round(houseAcPowerW)}W`);
reason.push(`Puissance réseau ${Math.round(gridPowerW)}W`);
reason.push(`EVCS ${evcsSeemsActive ? 'actif' : 'inactif'} | ${Math.round(evcsAcPowerW)}W | status_raw=${Number.isFinite(evcsStatusRaw) ? evcsStatusRaw : 'n/a'}`);
if (minutesSinceLastOn !== null) {
reason.push(`Dernier ON il y a ${round1(minutesSinceLastOn)} min`);
}
if (minutesSinceLastOff !== null) {
reason.push(`Dernier OFF il y a ${round1(minutesSinceLastOff)} min`);
}
if (acOverSafeLimit) {
reason.push(`⚠️ Charge AC maison ${Math.round(houseAcPowerW)}W >= limite sûre ${Math.round(acLoadsMaxSafeW)}W`);
}
// ======================================================
// Notifications
// ======================================================
let notifyTitle = 'Victron Grid Hysteresis';
let notifyMessage = '';
let notifyWanted = false;
if (state === 'turn_on_low_soc' || state === 'turn_on_low_soc_cooldown_bypassed') {
notifyTitle = '⚡ EDF activé sur SOC bas';
notifyMessage = `SOC ${round1(soc)}% <= ${gridSocOn}%. EDF activé et maintenu jusqu'à au moins ${gridSocOff}% (min ${gridMinOnMinutes} min).`;
notifyWanted = true;
} else if (state === 'adopt_existing_grid_on_low_soc' || state === 'adopt_existing_grid_on') {
notifyTitle = '🔁 EDF ON repris par hystérésis';
notifyMessage = `EDF était déjà ON. Le workflow reprend la main et le maintient jusqu'à ${gridSocOff}% (min ${gridMinOnMinutes} min).`;
notifyWanted = true;
} else if (state === 'turn_off_recovered') {
notifyTitle = '✅ EDF coupé après recharge';
notifyMessage = `SOC ${round1(soc)}% >= ${gridSocOff}%. Retour hors réseau autorisé.`;
notifyWanted = true;
} else if (state === 'turn_off_stray_grid_on') {
notifyTitle = '✅ EDF coupé (reprise de contrôle)';
notifyMessage = `EDF était ON sans latch. SOC ${round1(soc)}% >= ${gridSocOff}%, coupure réseau appliquée.`;
notifyWanted = true;
} else if (acOverSafeLimit && !gridIsOn) {
notifyTitle = '🚨 Charge AC élevée hors réseau';
notifyMessage = `Charge AC maison ${Math.round(houseAcPowerW)}W au-dessus de la limite sûre ${Math.round(acLoadsMaxSafeW)}W alors que EDF est OFF.`;
notifyWanted = true;
} else if (acOverSafeLimit && gridIsOn) {
notifyTitle = '⚠️ Charge AC élevée';
notifyMessage = `Charge AC maison ${Math.round(houseAcPowerW)}W proche/au-dessus de la limite sûre ${Math.round(acLoadsMaxSafeW)}W.`;
notifyWanted = true;
}
const shortReason = reason.join(' | ').slice(0, 255);
const signature = [
now.date,
state,
`dg=${desiredGrid}`,
`dl=${desiredLatch}`,
`sb=${socBand}`,
`ac=${acOverSafeLimit ? '1' : '0'}`
].join('|');
const signatureChanged = signature !== lastSignature;
const result = {
timestamp_paris: now.stamp,
inputs: {
soc_percent: soc,
grid_state: gridIsOn ? 'on' : 'off',
pv_power_w: pvPowerW,
house_ac_power_w: houseAcPowerW,
grid_power_w: gridPowerW,
evcs_ac_power_w: evcsAcPowerW,
evcs_charging: evcsCharging,
evcs_status_raw: Number.isFinite(evcsStatusRaw) ? evcsStatusRaw : null,
hysteresis_enabled: hysteresisEnabled,
notifications_enabled: notificationsEnabled,
grid_latch: latchIsOn,
grid_soc_on: gridSocOn,
grid_soc_off: gridSocOff,
grid_min_on_minutes: gridMinOnMinutes,
grid_cooldown_minutes: gridCooldownMinutes,
ac_loads_max_safe_w: acLoadsMaxSafeW,
last_on_at: lastOnAt || null,
last_off_at: lastOffAt || null,
last_signature: lastSignature || null
},
computed: {
desired_grid: desiredGrid,
desired_latch: desiredLatch,
minutes_since_last_on: minutesSinceLastOn !== null ? round1(minutesSinceLastOn) : null,
minutes_since_last_off: minutesSinceLastOff !== null ? round1(minutesSinceLastOff) : null,
min_on_elapsed: minOnElapsed,
cooldown_elapsed: cooldownElapsed,
ac_over_safe_limit: acOverSafeLimit,
evcs_seems_active: evcsSeemsActive
},
decision: {
state,
grid_action: gridAction,
latch_action: latchAction,
set_last_on_at: setLastOnAt,
set_last_off_at: setLastOffAt,
should_notify: notificationsEnabled && signatureChanged && notifyWanted,
signature,
signature_changed: signatureChanged,
reason,
short_reason: shortReason
},
ha_service_calls: {
helper_state: {
domain: 'input_text',
service: 'set_value',
data: {
entity_id: 'input_text.victron_grid_control_state',
value: state
}
},
helper_reason: {
domain: 'input_text',
service: 'set_value',
data: {
entity_id: 'input_text.victron_grid_control_reason',
value: shortReason || 'Aucune raison'
}
},
helper_signature: {
domain: 'input_text',
service: 'set_value',
data: {
entity_id: 'input_text.victron_grid_control_last_signature',
value: signature
}
},
helper_latch: {
domain: 'input_boolean',
service: desiredLatch === 'on' ? 'turn_on' : 'turn_off',
data: {
entity_id: 'input_boolean.victron_grid_hysteresis_latch'
}
},
helper_last_on_at: {
domain: 'input_datetime',
service: 'set_datetime',
data: {
entity_id: 'input_datetime.victron_grid_last_on_at',
datetime: now.stamp
}
},
helper_last_off_at: {
domain: 'input_datetime',
service: 'set_datetime',
data: {
entity_id: 'input_datetime.victron_grid_last_off_at',
datetime: now.stamp
}
},
grid: {
domain: 'switch',
service: desiredGrid === 'on' ? 'turn_on' : 'turn_off',
data: {
entity_id: 'switch.edf_general_ok_switch'
}
},
logbook: {
domain: 'logbook',
service: 'log',
data: {
name: 'Victron Grid Hysteresis',
message: `${state} | Grid=${desiredGrid} | Latch=${desiredLatch} | SOC=${round1(soc)}% | AC=${Math.round(houseAcPowerW)}W | ${shortReason}`,
entity_id: 'switch.edf_general_ok_switch'
}
},
mobile_notification: {
domain: cfg.mobileNotifyDomain,
service: cfg.mobileNotifyService,
data: {
title: notifyTitle,
message: notifyMessage,
data: {
push: {
"interruption-level": "time-sensitive",
sound: { name: "default" }
},
ttl: 0,
priority: "high"
}
}
}
}
};
return [{ json: result }];