// ====================================================== // 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 }];