225 lines
6.9 KiB
JavaScript
225 lines
6.9 KiB
JavaScript
// ======================================================
|
|
// NOTIFICATIONS
|
|
// ======================================================
|
|
let notifyTitle = 'Victron Grid Hysteresis';
|
|
let notifyMessage = '';
|
|
let notifyWanted = false;
|
|
|
|
if (
|
|
state === 'turn_on_evcs_override' ||
|
|
state === 'adopt_existing_grid_on_evcs_override' ||
|
|
state === 'hold_on_evcs_override'
|
|
) {
|
|
notifyTitle = '🚗 EDF forcé par EVCS';
|
|
notifyMessage = `Priorité EVCS active. EDF maintenu ON pour la recharge véhicule. Cause: ${userStrategyRaw || evcsSourceModeRaw || 'override EVCS'}.`;
|
|
notifyWanted = true;
|
|
|
|
} else 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'}`,
|
|
`ov=${evcsGridOverride ? '1' : '0'}`,
|
|
`ep=${evcsPriorityActive ? '1' : '0'}`,
|
|
`es=${evcsSurplusState || 'na'}`
|
|
].join('|');
|
|
|
|
const signatureChanged = signature !== lastSignature;
|
|
|
|
// ======================================================
|
|
// OUTPUT
|
|
// ======================================================
|
|
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,
|
|
|
|
evcs_priority_active: evcsPriorityActive,
|
|
night_grid_takeover_needed: evcsNightGridTakeoverNeeded,
|
|
evcs_source_mode: evcsSourceModeRaw || null,
|
|
user_strategy: userStrategyRaw || null,
|
|
evcs_surplus_state: evcsSurplusStateRaw || 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,
|
|
|
|
evcs_grid_override: evcsGridOverride,
|
|
evcs_priority_active: evcsPriorityActive,
|
|
evcs_night_grid_takeover_needed: evcsNightGridTakeoverNeeded,
|
|
evcs_source_mode: evcsSourceModeRaw || null,
|
|
evcs_user_strategy: userStrategyRaw || null,
|
|
evcs_surplus_state: evcsSurplusStateRaw || null,
|
|
evcs_vehicle_no_demand_semantic: evcsVehicleNoDemandSemantic
|
|
},
|
|
|
|
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 }];
|
|
|