- Nom du scénario : Surplus Solaire Batterie ECS - Mode du scénario : provoke - Evènement : #[Maison][Synthèse Energie][Conso Reseau]# CODE (code) /* ============================================== GESTION BATTERIE ZENDURE SolarFlow 800 Plus + ECS Version : 4.3.25 Auteur : Ngm47 23/03/2026 ============================================== */ $trigger_type = '#trigger#'; $trigger_id = '#trigger_id#'; $trigger_value = '#trigger_value#'; if ($trigger_type == 'virtualCmd' && $trigger_id == '16179' && $trigger_value == '1') { $last_binaire_trigger = (int) $scenario->getData('last_binaire_trigger'); $now_trigger = time(); if ($last_binaire_trigger && ($now_trigger - $last_binaire_trigger) < 30) { $scenario->setLog("⏭️ Skip déclenchement (il y a " . ($now_trigger - $last_binaire_trigger) . "s - trop récent)"); return; } $conso_reseau = (float) cmd::byId($ID_conso_reseau)->execCmd(); $scenario->setLog("⚡ Déclenchement trigger binaire conso réseau ({$conso_reseau}W)"); $scenario->setData('last_binaire_trigger', $now_trigger); $scenario->setData('last_trigger_time', $now_trigger); } /* ======================================== PARAMETRES CONFIGURABLES ======================================== */ // --- GENERAL --- $delai_max_compteur = 180; // délai max sans mise à jour compteur avant arrêt sécurité (s) $seuil_demarrage_on = 80; // surplus minimum pour démarrer la charge batterie (W) - hystérésis ON $seuil_demarrage_off = 50; // surplus minimum pour maintenir la charge batterie (W) - hystérésis OFF $min_power = 60; // puissance minimale en dessous de laquelle une commande est ignorée (W) // --- BATTERIE --- $soc_min_decharge = 20; // début plage ralentissement progressif décharge → jusqu'à $soc_min_securite (12% lu Zendure ID 16341) $soc_urgence_recharge = 6; // SOC critique → recharge forcée depuis le réseau (%) $soc_urgence_fin_recharge = 10; // SOC cible fin de recharge urgence (%) $p_recharge_urgence = 100; // puissance de recharge réseau en mode urgence (W) $bat_max_charge = 1000; // puissance max de charge batterie Zendure (W) $bat_max_decharge = 600; // puissance max de décharge batterie Zendure (W) $coef_charge_bat = 0.95; // coefficient d'utilisation du surplus pour la charge (évite injection réseau) // --- DECHARGE --- $seuil_conso_decharge = 50; // conso réseau minimum pour déclencher la décharge (W) $coef_decharge_conso = 0.85; // coefficient appliqué sur la conso pour calculer la puissance de décharge // ex: conso=300W → bat=(300-marge)*0.85=170W → réseau reste ~130W $marge_decharge = 100; // marge réseau conservée pendant la décharge - évite injection (W) // ex: conso=300W → bat=(300-100)*0.85=170W → réseau≈130W // --- ECS PALIERS (puissances réelles mesurées) --- $puissance_R3 = 750; // résistance R3 seule (W) $puissance_R2 = 1150; // résistance R2 seule (W) $puissance_R1 = 2220; // résistance R1 seule (W) $puissance_circ = 40; // circulateur de brassage (W) // --- SEUILS ECS - zone morte anti-oscillations (hystérésis ON/OFF) --- $seuil_R3_on = 850; $seuil_R3_off = 760; // R3 : démarre à 850W surplus, s'arrête sous 760W $seuil_R2_on = 1300; $seuil_R2_off = 1200; // R2 : démarre à 1300W surplus, s'arrête sous 1200W $seuil_R1_on = 2300; $seuil_R1_off = 2230; // R1 : démarre à 2350W surplus, s'arrête sous 2250W // --- CIRCULATEUR ECS --- $temp_ecs_min_circ = 65; // température mini pour démarrer le circulateur (°C) - seuil strict >= $anti_marteau_delai = 600; // délai minimum entre deux démarrages circulateur (s) - anti-usure $timeout_circ_max = 900; // durée max de fonctionnement circulateur par cycle (s) - 15min // --- GRIDC - anti-injection réseau --- $seuil_gridc_detection = 40; // conso réseau à partir de laquelle GridC surveille (W) $seuil_gridc_ajustement = 80; // conso réseau déclenchant un recalcul immédiat si GridC actif (W) $marge_gridc = 50; // marge conservée côté réseau lors des ajustements GridC (W) $seuil_tolerance_ajustc = 50; // résiduel acceptable sans couper l'ECS - étape [C] (W) $timer_gridc_ok_max = 30; // durée d'export stable (<0W) avant levée ajustement GridC (s) $delai_min_ecs_change = 60; // délai minimum entre deux changements de palier ECS - anti-oscillation (s) $timeout_gridc_off = 60; // délai avant libération GridC si conso réseau faible mais non nulle (s) $timeout_gridc_off_zero = 10; // délai avant libération GridC si conso réseau = 0W exactement (s) // plus rapide car pas de risque d'oscillation avec conso nulle $timeout_gridc_a_stable = 60; // durée pendant laquelle le plafond GridC-A est maintenu sans recalcul (s) // évite les ajustements en cascade quand conso réseau oscille toutes les 1-3s $seuil_gridc_urgence = 500; // conso réseau au-delà de laquelle la stabilisation [A] est annulée (W) // sécurité : force recalcul si gros appareil s'allume pendant stabilisation $timeout_ecs_reactivation = 60; // délai minimum avant réactivation ECS après coupure GridC [C] (s) // coupure reste instantanée, seule la réactivation est retardée // --- MODE NUAGEUX --- $seuil_inversions_entree = 2; // nb inversions de tendance solaire min par cycle pour le considérer instable $seuil_amplitude_nuageux = 600; // amplitude max des variations min pour valider instabilité - évite bruit (W) $duree_entree_nuageux = 3; // nb de cycles instables consécutifs avant activation mode nuageux $duree_sortie_nuageux = 10; // nb de cycles stables consécutifs avant désactivation mode nuageux $surplus_min_sortie_ecs = 1000; // surplus solaire minimum pour autoriser la sortie du mode nuageux (W) $timeout_sortie_nuageux = 60; // délai transition R3-only après sortie mode nuageux (s) // --- PROTECTION TEMPERATURE BATTERIE --- $temp_bat_charge_min = 0; // en dessous : charge interdite - risque gel (°C) $temp_bat_charge_max = 55; // au dessus : charge ET décharge interdites - sécurité (°C) $temp_bat_alerte = 45; // au dessus : charge limitée à 400W - batterie chaude (°C) // --- PROTECTION TENSION BATTERIE --- $tension_bat_min = 46.4; // tension min cohérente avec le SOC - alerte dérive BMS (V) /* ======================================== IDs CMD & INFO ======================================== */ // --- COMPTEURS ENERGIE (Synthèse Energie) --- $ID_injecte = 4442; // puissance injectée sur le réseau (W) - négatif = on tire du réseau $ID_prod_solaire = 10914; // production solaire instantanée (W) $ID_conso_reseau = 10916; // consommation tirée du réseau (W) - trigger virtualCmd sur changement $ID_etat_systeme = 16361; // virtuel texte affiché dans Jeedom (état courant du scénario) $ID_compteur = 11260; // horodatage dernière mise à jour compteur - watchdog // --- ECS (Chauffe-eau solaire) --- $ID_temp_ecs = 11416; // température eau ECS (°C) $ID_conso_ecs = 11415; // puissance consommée par les résistances ECS (W) $ID_thermostat_ecs = 11608; // thermostat ECS : 1=besoin de chauffe, 0=température atteinte $ID_mode_auto_routeur = 11693; // mode automatique routeur : 1=actif, 0=désactivé // --- BATTERIE ZENDURE SolarFlow --- $ID_p_bat_charge = 15899; // puissance de charge batterie en cours (W) $ID_p_bat_decharge = 15898; // puissance de décharge batterie en cours (W) $ID_soc = 15895; // état de charge batterie (%) $ID_mode_bat = 16176; // mode actuel : 0=veille, 1=charge, 2=décharge, 3=pleine $ID_temp_bat = 16173; // température interne batterie (°C) $ID_tension_bat = 16235; // tension batterie (V) - surveillance dérive BMS $ID_mode_charge = 15893; // commande : passer en mode charge $ID_mode_decharge = 15894; // commande : passer en mode décharge $ID_puissance_charge = 15892; // commande slider : puissance de charge (W) $ID_puissance_decharge = 15905; // commande slider : puissance de décharge (W) // --- HORAIRES SOLEIL (lever/coucher) --- $ID_heure_coucher_soleil = 16329; // heure coucher soleil - début période décharge (format HHMM) $ID_heure_lever_soleil = 16328; // heure lever soleil - fin période décharge (format HHMM) // --- RELAIS ECS (R1=2200W / R2=1150W / R3=750W) --- $ID_etatR1 = 12682; $ID_cmdR1_ON = 12681; $ID_cmdR1_OFF = 12680; // R1 état + ON + OFF $ID_etatR2 = 12685; $ID_cmdR2_ON = 12684; $ID_cmdR2_OFF = 12683; // R2 état + ON + OFF $ID_etatR3 = 12688; $ID_cmdR3_ON = 12687; $ID_cmdR3_OFF = 12686; // R3 état + ON + OFF // --- CIRCULATEUR ECS --- $ID_etatCirc = 12694; // état circulateur (bool) $ID_cmdCircON = 12693; // commande ON circulateur $ID_cmdCircOFF = 12692; // commande OFF circulateur /* ======================================== FONCTIONS HELPER ======================================== */ function setBatCharge($p_cible, $p_actuelle, $mode_bat, $force_resync, $ID_mode_charge, $ID_puissance_charge, $ID_puissance_decharge) { $change = false; if ($mode_bat != 1) { cmd::byId($ID_mode_charge)->execCmd(); $change = true; } if ((int)$p_cible != (int)$p_actuelle || $force_resync) { cmd::byId($ID_puissance_charge)->execCmd(['slider' => $p_cible]); cmd::byId($ID_puissance_decharge)->execCmd(['slider' => 0]); $change = true; } return $change; } function setBatDecharge($p_cible, $p_actuelle, $mode_bat, $force_resync, $ID_mode_decharge, $ID_puissance_decharge, $ID_puissance_charge) { $change = false; if ($mode_bat != 2) { cmd::byId($ID_mode_decharge)->execCmd(); $change = true; } if ((int)$p_cible != (int)$p_actuelle || $force_resync) { cmd::byId($ID_puissance_decharge)->execCmd(['slider' => $p_cible]); cmd::byId($ID_puissance_charge)->execCmd(['slider' => 0]); $change = true; } return $change; } function stopBatCharge($p_actuelle, $mode_bat, $ID_puissance_charge) { if ($p_actuelle > 0 || $mode_bat == 1) { cmd::byId($ID_puissance_charge)->execCmd(['slider' => 0]); return true; } return false; } function stopBatDecharge($p_actuelle, $mode_bat, $ID_puissance_decharge) { if ($p_actuelle > 0) { cmd::byId($ID_puissance_decharge)->execCmd(['slider' => 0]); return true; } return false; } function relaisON($etat, $ID_ON, $force_resync) { if (!$etat || $force_resync) { cmd::byId($ID_ON)->execCmd(); return true; } return false; } function relaisOFF($etat, $ID_OFF, $force_resync) { if ($etat || $force_resync) { cmd::byId($ID_OFF)->execCmd(); return true; } return false; } function getDataBool($scenario, $key) { return in_array($scenario->getData($key), [true, 1, '1', 'true'], true); } /* ======================================== CODE PRINCIPAL ======================================== */ $now_dt = new DateTime(); $last_update = new DateTime(cmd::byId($ID_compteur)->getCollectDate()); $delai = $now_dt->getTimestamp() - $last_update->getTimestamp(); $scenario->setLog("===== DEMARRAGE " . date('H:i:s') . " =========="); $now = time(); $force_resync = false; $etat_systeme = 'Veille'; if ($trigger_type == 'virtualCmd' && $trigger_id == '16179') { $last_trigger_time = (int)($scenario->getData('last_trigger_time') ?: 0); $gap = $now - $last_trigger_time; if ($gap < 5) { $scenario->setLog("⏭️ Trigger virtualCmd ignoré ({$gap}s < 5s - Zendure pas encore à jour)"); return; } $scenario->setData('last_trigger_time', $now); } if ($delai > $delai_max_compteur) { $scenario->setLog("[!!] Compteur HS ({$delai}s) - Arrêt sécurité"); message::add('Surplus', "ALERTE Compteur HS " . round($delai/60) . "min"); cmd::byId($ID_puissance_charge)->execCmd(['slider' => 0]); cmd::byId($ID_puissance_decharge)->execCmd(['slider' => 0]); cmd::byId($ID_cmdR1_OFF)->execCmd(); cmd::byId($ID_cmdR2_OFF)->execCmd(); cmd::byId($ID_cmdR3_OFF)->execCmd(); cmd::byId($ID_cmdCircOFF)->execCmd(); $scenario->setLog("[!!] Arrêt sécurité effectué"); return; } /* ======================================== LECTURE DONNEES ======================================== */ $injecte = (float) cmd::byId($ID_injecte)->execCmd(); $conso_reseau = (float) cmd::byId($ID_conso_reseau)->execCmd(); $p_bat_charge = (float) cmd::byId($ID_p_bat_charge)->execCmd(); $p_bat_decharge = (float) cmd::byId($ID_p_bat_decharge)->execCmd(); $soc = (float) cmd::byId($ID_soc)->execCmd(); $mode_bat = (int) cmd::byId($ID_mode_bat)->execCmd(); $temp_bat = (float) cmd::byId($ID_temp_bat)->execCmd(); $tension_bat = (float) cmd::byId($ID_tension_bat)->execCmd(); $temp_ecs = (float) cmd::byId($ID_temp_ecs)->execCmd(); $conso_ecs = (float) cmd::byId($ID_conso_ecs)->execCmd(); $thermostat_ecs = (int) cmd::byId($ID_thermostat_ecs)->execCmd(); $mode_auto_routeur = (int) cmd::byId($ID_mode_auto_routeur)->execCmd(); $etatR1 = (bool) cmd::byId($ID_etatR1)->execCmd(); $etatR2 = (bool) cmd::byId($ID_etatR2)->execCmd(); $etatR3 = (bool) cmd::byId($ID_etatR3)->execCmd(); $soc_zendure_min = (int) cmd::byId(16341)->execCmd(); $soc_zendure_max = (int) cmd::byId(16342)->execCmd(); $soc_min_securite = $soc_zendure_min; $soc_max_charge = $soc_zendure_max; $soc_optimal_min = $soc_zendure_min; $soc_optimal_max = $soc_zendure_max; $etatCirc = (bool) cmd::byId($ID_etatCirc)->execCmd(); $raw_debut_decharge = (int) cmd::byId($ID_heure_coucher_soleil)->execCmd(); $raw_fin_decharge = (int) cmd::byId($ID_heure_lever_soleil)->execCmd(); $heure_debut_decharge = (int)($raw_debut_decharge / 100) * 60 + ($raw_debut_decharge % 100); $heure_fin_decharge = (int)($raw_fin_decharge / 100) * 60 + ($raw_fin_decharge % 100); $label_debut_decharge = sprintf('%02d:%02d', (int)($raw_debut_decharge / 100), $raw_debut_decharge % 100); $label_fin_decharge = sprintf('%02d:%02d', (int)($raw_fin_decharge / 100), $raw_fin_decharge % 100); if (!is_numeric($injecte) || !is_numeric($conso_reseau) || !is_numeric($soc)) { $scenario->setLog("ERREUR lecture donnees"); return; } /* ======================================== CALCUL SURPLUS ======================================== */ $surplus_mesure = $injecte + $p_bat_charge - $p_bat_decharge; $conso_circ = $etatCirc ? $puissance_circ : 0; $surplus_total = $surplus_mesure + $conso_ecs + $conso_circ; $heure_actuelle = (int)date('G') * 60 + (int)date('i'); $mode_bat_label = ['0' => 'veille', '1' => 'charge', '2' => 'decharge', '3' => 'pleine']; if (($etatR1 || $etatR2 || $etatR3) && $conso_ecs < 100) { $scenario->setLog("⚠️ ECS relais ON mais consomme seulement {$conso_ecs}W - thermostat de sécurité ?"); } /* ======================================== DIAGNOSTIC INSTABILITE SOLAIRE ======================================== */ $diag_hist_raw = $scenario->getData('diag_surplus_hist'); if (is_array($diag_hist_raw)) { $diag_hist = $diag_hist_raw; } elseif (is_string($diag_hist_raw) && $diag_hist_raw !== '') { $diag_hist = json_decode($diag_hist_raw, true) ?: []; } else { $diag_hist = []; } $trigger_tags = $scenario->getTags() ?? []; $est_trigger_cron = isset($trigger_tags['#trigger_id#']) && $trigger_tags['#trigger_id#'] == '206'; $diag_prod_solaire = (float) cmd::byId($ID_prod_solaire)->execCmd(); if ($est_trigger_cron) { if ($diag_prod_solaire > 50) { $diag_hist[] = (int) round($diag_prod_solaire); if (count($diag_hist) > 6) array_shift($diag_hist); $scenario->setData('diag_surplus_hist', json_encode(array_values($diag_hist))); } } $diag_nb_inversions = 0; $diag_amp_max = 0; $diag_tendance = 'N/A'; if (count($diag_hist) >= 3) { $diag_deltas = []; $diag_signes = []; for ($i = 1; $i < count($diag_hist); $i++) { $delta = $diag_hist[$i] - $diag_hist[$i - 1]; $diag_deltas[] = $delta; $diag_amp_max = max($diag_amp_max, abs($delta)); $diag_signes[] = ($delta > 20) ? 1 : (($delta < -20) ? -1 : 0); } for ($i = 1; $i < count($diag_signes); $i++) { if ($diag_signes[$i] != 0 && $diag_signes[$i - 1] != 0 && $diag_signes[$i] != $diag_signes[$i - 1]) { $diag_nb_inversions++; } } $delta_global = end($diag_hist) - reset($diag_hist); if ($diag_nb_inversions <= 1) { $diag_tendance = ($delta_global > 50) ? 'montee' : (($delta_global < -50) ? 'descente' : 'stable'); } else { $diag_tendance = 'oscillation'; } $hist_str = implode('->', $diag_hist); $scenario->setLog("☀️ prod={$diag_prod_solaire}W hist={$hist_str} | inv={$diag_nb_inversions} ampMax={$diag_amp_max}W ⛅tendance={$diag_tendance}"); } /* ======================================== MODE NUAGEUX ======================================== */ $mode_nuageux = getDataBool($scenario, 'mode_nuageux'); $cpt_instable = (int)($scenario->getData('cpt_instable') ?: 0); $cpt_stable = (int)($scenario->getData('cpt_stable') ?: 0); if ($cpt_instable < 0) { $cpt_instable = 0; } if ($cpt_stable < 0) { $cpt_stable = 0; } $pas_de_soleil = ($diag_prod_solaire <= 50); if ($pas_de_soleil && count($diag_hist) > 0) { $diag_hist = []; $scenario->setData('diag_surplus_hist', json_encode([])); } if ($mode_nuageux && $pas_de_soleil) { $mode_nuageux = false; $cpt_stable = 0; $cpt_instable = 0; $scenario->setData('mode_nuageux', 0); $scenario->setData('nuageux_sortie_since', 0); $scenario->setLog("✅ MODE NUAGEUX désactivé - nuit (prod={$diag_prod_solaire}W)"); } $cycle_instable = ( count($diag_hist) >= 3 && $diag_nb_inversions >= $seuil_inversions_entree && $diag_amp_max >= $seuil_amplitude_nuageux ); if ($cycle_instable) { if ($est_trigger_cron) { $cpt_instable++; $cpt_stable = 0; } if (!$mode_nuageux && $cpt_instable >= $duree_entree_nuageux) { $mode_nuageux = true; $scenario->setData('mode_nuageux', 1); $scenario->setData('nuageux_sortie_since', 0); $scenario->setLog("⛅ MODE NUAGEUX activé ({$cpt_instable} cycles | inv={$diag_nb_inversions} ampMax={$diag_amp_max}W)"); } elseif ($mode_nuageux) { $scenario->setLog("⛅ Mode nuageux maintenu (inv={$diag_nb_inversions} ampMax={$diag_amp_max}W)"); } } else { if ($est_trigger_cron) { $cpt_stable++; $cpt_instable = 0; } if ($mode_nuageux) { if ($cpt_stable >= $duree_sortie_nuageux && $surplus_total >= $surplus_min_sortie_ecs) { $mode_nuageux = false; $scenario->setData('mode_nuageux', 0); $scenario->setData('nuageux_sortie_since', $now); $scenario->setLog("☀️ MODE NUAGEUX désactivé ({$cpt_stable} cycles stables | surplus={$surplus_total}W) - transition R3 {$timeout_sortie_nuageux}s"); } else { $reste_cycles = $duree_sortie_nuageux - $cpt_stable; $scenario->setLog("⛅ Mode nuageux maintenu - sortie dans {$reste_cycles} cycles stables (surplus={$surplus_total}W)"); } } } $scenario->setData('cpt_instable', $cpt_instable); $scenario->setData('cpt_stable', $cpt_stable); // FIX v4.3.16 : période de transition post-nuageux — R3 max pendant $timeout_sortie_nuageux secondes $nuageux_sortie_since = (int)($scenario->getData('nuageux_sortie_since') ?: 0); $en_transition_nuageux = ($nuageux_sortie_since > 0 && ($now - $nuageux_sortie_since) < $timeout_sortie_nuageux); if ($en_transition_nuageux) { $reste_transition = $timeout_sortie_nuageux - ($now - $nuageux_sortie_since); $scenario->setLog("☀️ Transition post-nuageux - R3 max encore {$reste_transition}s"); } elseif ($nuageux_sortie_since > 0 && !$mode_nuageux) { $scenario->setData('nuageux_sortie_since', 0); $nuageux_sortie_since = 0; } /* ===== FIN MODE NUAGEUX ===== */ /* ======================================== PROTECTION TEMPERATURE BATTERIE ======================================== */ $charge_interdite_temp = false; if ($temp_bat < $temp_bat_charge_min) { $charge_interdite_temp = true; stopBatCharge($p_bat_charge, $mode_bat, $ID_puissance_charge); $scenario->setLog("[!!] Charge interdite - Batterie trop froide ({$temp_bat}C < {$temp_bat_charge_min}C minimum)"); $last_alerte_temp = (int) $scenario->getData('last_alerte_temp'); if (!$last_alerte_temp || ($now - $last_alerte_temp) > 3600) { message::add('Batterie', "Charge interdite - Temperature batterie {$temp_bat}C (gel)"); $scenario->setData('last_alerte_temp', $now); } } elseif ($temp_bat > $temp_bat_charge_max) { $charge_interdite_temp = true; stopBatCharge($p_bat_charge, $mode_bat, $ID_puissance_charge); stopBatDecharge($p_bat_decharge, $mode_bat, $ID_puissance_decharge); $scenario->setLog("[!!] Batterie bloquée charge ET décharge - Température critique ({$temp_bat}C > {$temp_bat_charge_max}C max)"); $last_alerte_temp = (int) $scenario->getData('last_alerte_temp'); if (!$last_alerte_temp || ($now - $last_alerte_temp) > 3600) { message::add('Batterie', "ALERTE Temperature critique batterie {$temp_bat}C !"); $scenario->setData('last_alerte_temp', $now); } goto circulateur; } elseif ($temp_bat > $temp_bat_alerte) { $bat_max_charge = 400; $scenario->setLog("⚠️ Charge limitée à 400W - Batterie chaude ({$temp_bat}C > {$temp_bat_alerte}C)"); } else { $scenario->setData('last_alerte_temp', 0); } /* ======================================== DETECTION DERIVE BMS ======================================== */ if ($tension_bat < $tension_bat_min && $soc > 15) { $scenario->setLog("⚠️ Dérive BMS détectée - Tension={$tension_bat}V incohérente avec SOC={$soc}%"); $last_alerte_bms = (int) $scenario->getData('last_alerte_bms'); if (!$last_alerte_bms || ($now - $last_alerte_bms) > 3600) { message::add('Batterie', "Derive BMS detectee - Tension {$tension_bat}V / SOC {$soc}%"); $scenario->setData('last_alerte_bms', $now); } } /* ======================================== ETAPE 1 : RECHARGE URGENCE RESEAU ======================================== */ $urgence_active = getDataBool($scenario, 'urgence_recharge_active'); if (!$urgence_active && $soc < $soc_urgence_recharge) { $urgence_active = true; $scenario->setData('urgence_recharge_active', 1); $scenario->setLog("[!!] URGENCE déclenchée - SOC critique {$soc}% < {$soc_urgence_recharge}%"); $last_alerte = (int) $scenario->getData('last_alerte_urgence'); if (!$last_alerte || ($now - $last_alerte) > 3600) { message::add('Batterie', "SOC critique ({$soc}%) - Recharge reseau activee"); $scenario->setData('last_alerte_urgence', $now); } } if ($urgence_active && $soc >= $soc_urgence_fin_recharge) { $urgence_active = false; $scenario->setData('urgence_recharge_active', 0); $scenario->setData('last_alerte_urgence', 0); $scenario->setLog("✅ Urgence terminée - SOC={$soc}% atteint"); } if ($urgence_active) { relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisOFF($etatR3, $ID_cmdR3_OFF, $force_resync); $etatR3 = false; stopBatDecharge($p_bat_decharge, $mode_bat, $ID_puissance_decharge); setBatCharge($p_recharge_urgence, $p_bat_charge, $mode_bat, $force_resync, $ID_mode_charge, $ID_puissance_charge, $ID_puissance_decharge); $scenario->setLog("[!!] URGENCE active | Recharge réseau {$p_recharge_urgence}W | SOC={$soc}% → cible {$soc_urgence_fin_recharge}%"); $etat_systeme = 'Veille'; goto circulateur; } /* ======================================== ETAPE 2 : DECHARGE INTELLIGENTE ======================================== */ $palier_ecs_actif = 0; $periode_decharge = ($heure_debut_decharge > $heure_fin_decharge) ? ($heure_actuelle >= $heure_debut_decharge || $heure_actuelle < $heure_fin_decharge) : ($heure_actuelle >= $heure_debut_decharge && $heure_actuelle < $heure_fin_decharge); if ($soc <= $soc_min_securite) { if (stopBatDecharge($p_bat_decharge, $mode_bat, $ID_puissance_decharge)) { $scenario->setLog("⚠️ Stop décharge - SOC critique ({$soc}% <= {$soc_min_securite}% minimum)"); } } $decharge_autorisee = ($periode_decharge && $soc > $soc_min_securite); if ($conso_reseau > $seuil_conso_decharge && $decharge_autorisee) { $coef = ($soc > $soc_min_decharge) ? 1.0 : max(0, min(1, ($soc - $soc_min_securite) / ($soc_min_decharge - $soc_min_securite))); $conso_reelle = $conso_reseau + $p_bat_decharge; $p_dech = (int) round(min($bat_max_decharge * $coef, ($conso_reelle - $marge_decharge) * $coef_decharge_conso)); if ($p_dech >= $min_power) { relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisOFF($etatR3, $ID_cmdR3_OFF, $force_resync); $etatR3 = false; $changed = setBatDecharge($p_dech, $p_bat_decharge, $mode_bat, $force_resync, $ID_mode_decharge, $ID_puissance_decharge, $ID_puissance_charge); $tag = $changed ? "[CHANGE]" : "[MAINTIEN]"; $scenario->setLog("⚡ Décharge {$p_dech}W | Conso={$conso_reseau}W BatDech={$p_bat_decharge}W ConsReelle={$conso_reelle}W | marge={$marge_decharge}W | SOC={$soc}% coef=" . round($coef, 2) . " {$tag}"); $etat_systeme = 'Décharge'; } else { stopBatDecharge($p_bat_decharge, $mode_bat, $ID_puissance_decharge); $scenario->setLog("ℹ️ Décharge trop faible ({$p_dech}W < {$min_power}W) - arrêt décharge"); } goto circulateur; } elseif ($conso_reseau > $seuil_conso_decharge && !$decharge_autorisee) { stopBatDecharge($p_bat_decharge, $mode_bat, $ID_puissance_decharge); $scenario->setLog("⏳ Décharge bloquée" . (!$periode_decharge ? " - hors période ({$label_debut_decharge}-{$label_fin_decharge})" : " - SOC={$soc}% trop faible")); } elseif ($conso_reseau <= $seuil_conso_decharge && $p_bat_decharge > 0) { $conso_reelle_dech = $conso_reseau + $p_bat_decharge; $p_dech_reduit = (int) round(($conso_reelle_dech - $marge_decharge) * $coef_decharge_conso); if ($p_dech_reduit >= $min_power) { $changed = setBatDecharge($p_dech_reduit, $p_bat_decharge, $mode_bat, $force_resync, $ID_mode_decharge, $ID_puissance_decharge, $ID_puissance_charge); $tag = $changed ? "[CHANGE]" : "[MAINTIEN]"; $scenario->setLog("⚡ Décharge réduite {$p_bat_decharge}W→{$p_dech_reduit}W (conso={$conso_reseau}W sous seuil={$seuil_conso_decharge}W) {$tag}"); $etat_systeme = 'Décharge'; } else { stopBatDecharge($p_bat_decharge, $mode_bat, $ID_puissance_decharge); $scenario->setLog("ℹ️ Décharge arrêtée - Conso réseau={$conso_reseau}W < seuil={$seuil_conso_decharge}W (réduction impossible {$p_dech_reduit}W < {$min_power}W)"); } } /* ======================================== ETAPE 3 : GRIDC INTELLIGENTE ======================================== */ $gridC_alerte_start = (int) ($scenario->getData('gridC_alerte_start') ?: 0); $gridc_ajuste = getDataBool($scenario, 'gridc_ajuste'); $gridc_ecs_forcee = (string) ($scenario->getData('gridc_ecs_forcee') ?: ''); $gridc_bat_max_adj = (int) ($scenario->getData('gridc_bat_max_adj') ?: 0); $gridc_ok_since = (int) ($scenario->getData('gridc_ok_since') ?: 0); $last_ecs_change = (int) ($scenario->getData('last_ecs_change') ?: 0); $gridc_a_stable_since = (int) ($scenario->getData('gridc_a_stable_since') ?: 0); $gridc_ecs_reac_since = (int) ($scenario->getData('gridc_ecs_reac_since') ?: 0); $gridc_action_faite = false; if ($decharge_autorisee) { goto apres_gridc; } if ($conso_reseau < $seuil_gridc_detection) { $gridc_off_since = (int)($scenario->getData('gridc_off_since') ?: 0); if ($gridc_ajuste) { $timeout_off_actif = ($conso_reseau <= 0) ? $timeout_gridc_off_zero : $timeout_gridc_off; if ($gridc_off_since == 0) { $scenario->setData('gridc_off_since', $now); $gridc_off_since = $now; $scenario->setLog("⏳ GridC-" . ($gridc_ecs_forcee == 'OFF' ? 'OFF' : 'A') . " timer démarré - libération dans {$timeout_off_actif}s" . ($conso_reseau <= 0 ? " (conso=0W)" : " (conso={$conso_reseau}W)")); } else { $duree_off = $now - $gridc_off_since; if ($duree_off >= $timeout_off_actif) { $scenario->setData('gridc_ajuste', 0); $scenario->setData('gridc_ecs_forcee', ''); $scenario->setData('gridc_bat_max_adj', 0); $scenario->setData('gridc_ok_since', 0); $scenario->setData('gridc_off_since', 0); $scenario->setData('gridC_alerte_start', 0); $scenario->setData('gridc_a_stable_since', 0); $scenario->setData('gridc_ecs_reac_since', 0); $scenario->setData('gridc_c_logue', 0); $gridc_ajuste = false; $gridc_ecs_forcee = ''; $gridc_bat_max_adj = 0; $gridC_alerte_start = 0; $scenario->setLog("✅ GridC expiré ({$duree_off}s >= {$timeout_off_actif}s) - ajustement levé, reprise normale"); } else { $reste = $timeout_off_actif - $duree_off; $scenario->setLog("⏳ GridC-" . ($gridc_ecs_forcee == 'OFF' ? 'OFF' : 'A bat=' . $gridc_bat_max_adj . 'W') . " actif depuis {$duree_off}s - libération dans {$reste}s"); } } } else { $scenario->setData('gridc_off_since', 0); } if ($gridC_alerte_start > 0) { $duree = $now - $gridC_alerte_start; $scenario->setLog("✅ Conso Réseau sous seuil ({$conso_reseau}W < {$seuil_gridc_detection}W) - alerte réinitialisée ({$duree}s)"); $scenario->setData('gridC_alerte_start', 0); $gridC_alerte_start = 0; } if ($gridc_ajuste || $gridc_ecs_forcee != '') { if ($conso_reseau < 0) { if ($gridc_ok_since == 0) { $scenario->setData('gridc_ok_since', $now); $gridc_ok_since = $now; } $duree_ok = $now - $gridc_ok_since; if ($duree_ok >= $timer_gridc_ok_max) { $scenario->setData('gridc_ajuste', 0); $scenario->setData('gridc_ecs_forcee', ''); $scenario->setData('gridc_bat_max_adj', 0); $scenario->setData('gridc_ok_since', 0); $scenario->setData('gridc_a_stable_since', 0); $scenario->setData('gridc_ecs_reac_since', 0); $scenario->setData('gridc_c_logue', 0); $gridc_ajuste = false; $gridc_ecs_forcee = ''; $gridc_bat_max_adj = 0; $scenario->setLog("✅ Export réseau stable {$duree_ok}s - ajustement levé, reprise normale"); } else { $remaining = $timer_gridc_ok_max - $duree_ok; $scenario->setLog("⏳ Export réseau OK depuis {$duree_ok}s - ajustement maintenu encore {$remaining}s"); } } else { $scenario->setData('gridc_ok_since', 0); $gridc_ok_since = 0; $scenario->setLog("⏳ GridC maintenu - Conso réseau={$conso_reseau}W (attente export <0W)"); } } else { $scenario->setData('gridc_ok_since', 0); } } else { $scenario->setData('gridc_ok_since', 0); $scenario->setData('gridc_off_since', 0); $gridc_ok_since = 0; } if ($conso_reseau >= $seuil_gridc_detection) { $recalcul_immediat = ($gridc_ajuste && $conso_reseau >= $seuil_gridc_ajustement); if ($recalcul_immediat && $gridc_bat_max_adj > 0 && $gridc_a_stable_since > 0) { $duree_stable = $now - $gridc_a_stable_since; if ($duree_stable < $timeout_gridc_a_stable) { if ($conso_reseau >= $seuil_gridc_urgence) { $scenario->setLog("⚠️ GridC-A stabilisation annulée - Conso urgence={$conso_reseau}W >= {$seuil_gridc_urgence}W"); } elseif ($conso_reseau > $gridc_bat_max_adj) { $scenario->setLog("⚠️ GridC-A stabilisation annulée - Conso={$conso_reseau}W > plafond={$gridc_bat_max_adj}W"); } elseif ($conso_reseau > $p_bat_charge) { $scenario->setLog("⚠️ GridC-A stabilisation annulée - Conso={$conso_reseau}W > bat_charge={$p_bat_charge}W → recalcul nécessaire"); } else { $reste_stable = $timeout_gridc_a_stable - $duree_stable; $bat_max_charge = min($bat_max_charge, $gridc_bat_max_adj); $scenario->setLog("⏳ GridC-A bat={$gridc_bat_max_adj}W actif depuis {$duree_stable}s - stabilisation {$reste_stable}s restantes"); $recalcul_immediat = false; goto apres_gridc; } } } if ($recalcul_immediat) { $gridC_alerte_start = $now; $scenario->setData('gridC_alerte_start', $now); $scenario->setLog("⚠️ Conso Réseau={$conso_reseau}W - recalcul immédiat (ajustement en cours)"); } elseif ($gridC_alerte_start == 0) { $gridC_alerte_start = $now; $scenario->setData('gridC_alerte_start', $now); if ($est_trigger_cron) { $scenario->setLog("⚠️ Conso Réseau={$conso_reseau}W détectée [cron] - action immédiate"); } else { $scenario->setLog("⚠️⏳ Conso Réseau={$conso_reseau}W détectée - confirmation dans 10s"); if (!$gridc_ajuste) { $palier_gel = $etatR1 ? 'R1' : ($etatR2 ? 'R2' : ($etatR3 ? 'R3' : '')); if ($palier_gel != '') { $gridc_ajuste = true; $gridc_ecs_forcee = $palier_gel; $scenario->setData('gridc_ajuste', 1); $scenario->setData('gridc_ecs_forcee', $palier_gel); $scenario->setLog(" ➜ ECS gelée à {$palier_gel} pendant confirmation"); } } goto apres_gridc; } } else { $duree_alerte = $now - $gridC_alerte_start; if ($duree_alerte < 10) { if ($est_trigger_cron) { $scenario->setLog("⚠️ Conso Réseau={$conso_reseau}W - action immédiate [cron] (était en attente {$duree_alerte}s)"); } else { $remaining = 10 - $duree_alerte; $scenario->setLog("⏳ Conso Réseau={$conso_reseau}W - confirmation dans {$remaining}s"); if ($gridc_bat_max_adj > 0) { $bat_max_charge = min($bat_max_charge, $gridc_bat_max_adj); $scenario->setLog("⏳ Batterie bridée à {$bat_max_charge}W pendant confirmation"); } goto apres_gridc; } } } $duree_alerte = $now - $gridC_alerte_start; $scenario->setLog("⚠️ Conso Réseau={$conso_reseau}W confirmée ({$duree_alerte}s) - cascade d'ajustements"); // ↓↓↓ CORRECTION v4.3.24 : surplus négatif ou nul - court-circuit avant cascade [A]/[B]/[C] ↓↓↓ if ($surplus_total <= 0) { if ($p_bat_charge > 0) { // Surplus négatif mais batterie encore en charge → on réduit ou coupe $bat_cible_surplus = (int)($p_bat_charge + $surplus_total - $marge_gridc); if ($bat_cible_surplus >= $min_power) { $bat_max_charge = $bat_cible_surplus; $scenario->setData('gridc_ajuste', 1); $scenario->setData('gridc_bat_max_adj', $bat_cible_surplus); $scenario->setData('gridc_ecs_forcee', ''); $scenario->setData('gridc_a_stable_since', $now); $gridc_ajuste = true; $gridc_ecs_forcee = ''; $gridc_bat_max_adj = $bat_cible_surplus; $gridc_a_stable_since = $now; $scenario->setLog(" ➜ [S] Surplus={$surplus_total}W → batterie réduite {$p_bat_charge}W→{$bat_cible_surplus}W | Réseau≈0W"); } else { stopBatCharge($p_bat_charge, $mode_bat, $ID_puissance_charge); $scenario->setData('gridc_ajuste', 1); $scenario->setData('gridc_ecs_forcee', ''); $scenario->setData('gridc_bat_max_adj', 0); $gridc_ajuste = true; $gridc_ecs_forcee = ''; $gridc_bat_max_adj = 0; $scenario->setLog(" ➜ [S] Surplus={$surplus_total}W → batterie coupée, conso réseau={$conso_reseau}W acceptée (maison seule)"); } } else { // Surplus nul ET batterie déjà à 0W → rien à faire, conso réseau inévitable $scenario->setLog("ℹ️ [S] Surplus={$surplus_total}W + batterie=0W → conso réseau={$conso_reseau}W inévitable, aucune action"); } goto apres_gridc; } // ↑↑↑ FIN CORRECTION v4.3.24 ↑↑↑ $bat_cible = (int)($p_bat_charge - $conso_reseau - $marge_gridc); $gridC_residuel = (float)($conso_reseau - $p_bat_charge); if ($bat_cible >= $min_power) { $bat_max_charge = $bat_cible; $scenario->setData('gridc_ajuste', 1); $scenario->setData('gridc_bat_max_adj', $bat_cible); $scenario->setData('gridc_ecs_forcee', ''); $scenario->setData('gridc_a_stable_since', $now); $gridc_ajuste = true; $gridc_ecs_forcee = ''; $gridc_bat_max_adj = $bat_cible; $gridc_a_stable_since = $now; $scenario->setLog(" ➜ [A] Batterie réduite {$p_bat_charge}W→{$bat_cible}W | ECS conservée"); } else { if ($mode_bat != 2) { stopBatCharge($p_bat_charge, $mode_bat, $ID_puissance_charge); } $scenario->setData('gridc_bat_max_adj', 0); $gridc_bat_max_adj = 0; $gridC_residuel_reel = max(0.0, $gridC_residuel); if ($gridC_residuel_reel <= 0) { $scenario->setData('gridc_ajuste', 1); $scenario->setData('gridc_ecs_forcee', ''); $gridc_ajuste = true; $gridc_ecs_forcee = ''; $scenario->setLog(" ➜ [A] Batterie coupée - suffit | ECS conservée"); $gridc_action_faite = true; } else { if ($etatR1) { $palier_actuel = 'R1'; } elseif ($etatR2) { $palier_actuel = 'R2'; } elseif ($etatR3) { $palier_actuel = 'R3'; } elseif ($gridc_ecs_forcee == 'R2') { $palier_actuel = 'R2'; } elseif ($gridc_ecs_forcee == 'R3') { $palier_actuel = 'R3'; } elseif ($conso_ecs >= $puissance_R1 * 0.7) { $palier_actuel = 'R1'; $scenario->setLog(" ➜ [B] Palier R1 détecté via ECS={$conso_ecs}W"); } elseif ($conso_ecs >= $puissance_R2 * 0.7) { $palier_actuel = 'R2'; $scenario->setLog(" ➜ [B] Palier R2 détecté via ECS={$conso_ecs}W"); } elseif ($conso_ecs >= $puissance_R3 * 0.7) { $palier_actuel = 'R3'; $scenario->setLog(" ➜ [B] Palier R3 détecté via ECS={$conso_ecs}W"); } else { $palier_actuel = ''; } $aucun_palier_actif = ($palier_actuel == ''); if (!$aucun_palier_actif) { $scenario->setLog(" ➜ [A] Batterie coupée (0W) | Résiduel={$gridC_residuel_reel}W → étape suivante"); $scenario->setLog(" ➜ [B] Palier=" . $palier_actuel . " | Résiduel={$gridC_residuel_reel}W"); } $ecs_forcee_new = ''; if ($palier_actuel == 'R1') { $gain_b = $conso_ecs - $puissance_R2; if (($gridC_residuel_reel - $gain_b) <= 0) { $ecs_forcee_new = 'R2'; $scenario->setLog(" ➜ [B] R1→R2 (gain={$gain_b}W)"); } else { $gain_b2 = $conso_ecs - $puissance_R3; if (($gridC_residuel_reel - $gain_b2) <= 0) { $ecs_forcee_new = 'R3'; $scenario->setLog(" ➜ [B] R1→R3 (gain={$gain_b2}W)"); } } } elseif ($palier_actuel == 'R2') { $gain_b = $conso_ecs - $puissance_R3; if (($gridC_residuel_reel - $gain_b) <= 0) { $ecs_forcee_new = 'R3'; $scenario->setLog(" ➜ [B] R2→R3 (gain={$gain_b}W)"); } } $est_montee = (($palier_actuel == 'R3' && $ecs_forcee_new == 'R2') || ($palier_actuel == 'R2' && $ecs_forcee_new == 'R1') || ($palier_actuel == 'R3' && $ecs_forcee_new == 'R1')); if ($ecs_forcee_new != '' && $est_montee && ($now - $last_ecs_change) < $delai_min_ecs_change) { $reste = $delai_min_ecs_change - ($now - $last_ecs_change); $scenario->setLog(" ➜ [B] Montée bloquée anti-oscillation ({$reste}s) - maintien {$palier_actuel}"); $ecs_forcee_new = ''; } if ($ecs_forcee_new != '') { $scenario->setData('last_ecs_change', $now); $last_ecs_change = $now; $scenario->setData('gridc_ajuste', 1); $scenario->setData('gridc_ecs_forcee', $ecs_forcee_new); $scenario->setData('gridc_bat_max_adj', 0); $gridc_ajuste = true; $gridc_ecs_forcee = $ecs_forcee_new; $gridc_action_faite = true; if ($ecs_forcee_new == 'R3') { relaisOFF($etatR1, $ID_cmdR1_OFF, true); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, true); $etatR2 = false; relaisON($etatR3, $ID_cmdR3_ON, true); $etatR3 = true; } elseif ($ecs_forcee_new == 'R2') { relaisOFF($etatR1, $ID_cmdR1_OFF, true); $etatR1 = false; relaisON($etatR2, $ID_cmdR2_ON, true); $etatR2 = true; relaisOFF($etatR3, $ID_cmdR3_OFF, true); $etatR3 = false; } $scenario->setLog(" ➜ [B] ECS forcée → {$ecs_forcee_new}"); } else { if ($aucun_palier_actif) { $gridc_action_faite = true; $gridc_c_deja_logue = getDataBool($scenario, 'gridc_c_logue'); $log_msg = ($gridc_ecs_forcee == 'OFF') ? " ➜ [C] Aucune ECS active - reset ajustement total" : " ➜ [C] Aucune action possible (ECS et batterie à 0W) - reset ajustement"; $scenario->setData('gridc_ecs_forcee', ''); $scenario->setData('gridc_ajuste', 0); $scenario->setData('gridc_bat_max_adj', 0); $scenario->setData('gridC_alerte_start', 0); $scenario->setData('gridc_ok_since', 0); $scenario->setData('gridc_c_logue', 1); $gridc_ecs_forcee = ''; $gridc_ajuste = false; $gridc_bat_max_adj = 0; $gridC_alerte_start = 0; if (!$gridc_c_deja_logue) { $scenario->setLog($log_msg); } } elseif ($gridC_residuel_reel <= $seuil_tolerance_ajustc) { $scenario->setData('gridc_ajuste', 1); $maintien = ($gridc_ecs_forcee != '') ? $gridc_ecs_forcee : $palier_actuel; $scenario->setData('gridc_ecs_forcee', $maintien); $gridc_ajuste = true; $gridc_ecs_forcee = $maintien; $gridc_action_faite = true; $scenario->setLog(" ➜ [C] ECS maintenue {$maintien} - résiduel {$gridC_residuel_reel}W < {$seuil_tolerance_ajustc}W"); } else { relaisOFF($etatR1, $ID_cmdR1_OFF, true); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, true); $etatR2 = false; relaisOFF($etatR3, $ID_cmdR3_OFF, true); $etatR3 = false; $scenario->setData('gridc_ajuste', 1); $scenario->setData('gridc_ecs_forcee', 'OFF'); $scenario->setData('gridc_bat_max_adj', 0); $scenario->setData('gridc_ecs_reac_since', $now); $gridc_ajuste = true; $gridc_ecs_forcee = 'OFF'; $gridc_ecs_reac_since = $now; $gridc_action_faite = true; $scenario->setLog(" ➜ [C] ECS coupée - dernier recours (Conso={$conso_reseau}W non couvrable)"); } } } } } apres_gridc: if ($gridc_bat_max_adj > 0 && !$gridc_action_faite) { $bat_max_charge = min($bat_max_charge, $gridc_bat_max_adj); } if ($gridc_ecs_reac_since > 0 && ($now - $gridc_ecs_reac_since) < $timeout_ecs_reactivation) { $bat_max_reac = max(0, $surplus_total - $puissance_R3); if ($bat_max_reac < $min_power) { $bat_max_reac = $bat_max_charge; } if ($bat_max_reac < $bat_max_charge) { $reste_reac = $timeout_ecs_reactivation - ($now - $gridc_ecs_reac_since); $bat_max_charge = $bat_max_reac; $scenario->setLog("⏳ Bat bridée {$bat_max_charge}W - réservation ECS post-[C] ({$reste_reac}s restantes)"); } } if ($gridc_action_faite) { $palier_ecs_actif = 0; goto circulateur; } /* ======================================== ETAPE 4 : PAS DE SURPLUS ======================================== */ $bat_etait_active = ($p_bat_charge > 0); $seuil_actif = $bat_etait_active ? $seuil_demarrage_off : $seuil_demarrage_on; if ($surplus_total < $seuil_actif) { if ($decharge_autorisee && $p_bat_decharge > 0) { goto circulateur; } if ($conso_reseau >= $seuil_gridc_detection) { $scenario->setLog("⏳ Surplus faible ({$surplus_total}W) avec Conso Réseau={$conso_reseau}W - attente"); stopBatCharge($p_bat_charge, $mode_bat, $ID_puissance_charge); if (!$decharge_autorisee) stopBatDecharge($p_bat_decharge, $mode_bat, $ID_puissance_decharge); $etat_systeme = 'Veille'; goto circulateur; } stopBatCharge($p_bat_charge, $mode_bat, $ID_puissance_charge); if (!$decharge_autorisee) stopBatDecharge($p_bat_decharge, $mode_bat, $ID_puissance_decharge); relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisOFF($etatR3, $ID_cmdR3_OFF, $force_resync); $etatR3 = false; if ($soc <= $soc_min_securite) { $scenario->setLog("⚠️ SOC critique ({$soc}%) - arrêt total"); } elseif ($conso_reseau > 0) { $scenario->setLog("ℹ️ Pas de surplus solaire | Conso Réseau={$conso_reseau}W"); } else { $scenario->setLog("ℹ️ Surplus insuffisant ({$surplus_total}W < seuil={$seuil_actif}W) - veille"); } $etat_systeme = 'Veille'; goto circulateur; } /* ======================================== ETAPE 5 : BATTERIE EN MODE NUAGEUX ======================================== */ $palier_ecs_actif = 0; $surplus_pour_bat = $surplus_total; $surplus_pour_ecs = $surplus_total; if ($mode_nuageux && !$gridc_ajuste) { $ecs_autorisee = ($thermostat_ecs == 1 && $mode_auto_routeur == 1); $bat_cible_nuageux = (int) round(min($bat_max_charge, $surplus_total * $coef_charge_bat)); $bat_lancee = false; if ($bat_cible_nuageux >= $min_power && $soc < $soc_max_charge && !$charge_interdite_temp) { $changed = setBatCharge($bat_cible_nuageux, $p_bat_charge, $mode_bat, $force_resync, $ID_mode_charge, $ID_puissance_charge, $ID_puissance_decharge); $tag = $changed ? "✅[CHANGE]" : "ℹ️[INCHANGE]"; $scenario->setLog("⚡ Bat charge {$bat_cible_nuageux}W (dispo={$surplus_total}W) [Nuageux] {$tag}"); $surplus_pour_ecs = $surplus_total - $bat_cible_nuageux; $bat_lancee = true; $etat_systeme = 'Charge - Prio Batterie (Ciel instable)'; } else { $surplus_pour_ecs = $surplus_total; $etat_systeme = 'Veille'; if ($soc >= $soc_max_charge) { $scenario->setLog("ℹ️ [Nuageux] Batterie pleine - surplus vers ECS si possible"); } } // FIX v4.3.16 : seuil R3 en mode nuageux basé sur puissance réelle (750W) + marge, pas seuil_R3_on (900W) $seuil_r3_nuageux = $puissance_R3 + 50; // 800W — cohérent avec la puissance réelle de R3 if ($ecs_autorisee && $surplus_pour_ecs >= $seuil_r3_nuageux) { relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisON($etatR3, $ID_cmdR3_ON, $force_resync); $etatR3 = true; $palier_ecs_actif = $conso_ecs; $scenario->setLog("⚡ ECS R3 ON {$conso_ecs}W (Ciel instable - palier max R3) | surplus restant={$surplus_pour_ecs}W [seuil={$seuil_r3_nuageux}W]"); if (!$bat_lancee) $etat_systeme = 'Charge - Prio ECS'; } else { relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisOFF($etatR3, $ID_cmdR3_OFF, $force_resync); $etatR3 = false; if ($ecs_autorisee) { $scenario->setLog("ECS OFF - surplus restant insuffisant après batterie ({$surplus_pour_ecs}W < {$seuil_r3_nuageux}W) [Nuageux]"); } else { $scenario->setLog($thermostat_ecs == 0 ? "ECS OFF - Thermostat satisfait (T={$temp_ecs}C)" : "ECS OFF - Mode auto désactivé"); } } goto circulateur; } /* ======================================== ETAPE 5 : ECS CASCADE (mode normal) FIX v4.3.16 : en transition post-nuageux → R3 max seulement ======================================== */ $ecs_autorisee = ($thermostat_ecs == 1 && $mode_auto_routeur == 1); $palier_ecs_actif = 0; $surplus_pour_bat = $surplus_total; // FIX v4.3.16 : bloquer R1/R2 pendant la transition post-nuageux if ($en_transition_nuageux) { $seuil_R1_on = PHP_INT_MAX; $seuil_R2_on = PHP_INT_MAX; } if (!$ecs_autorisee) { relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisOFF($etatR3, $ID_cmdR3_OFF, $force_resync); $etatR3 = false; $scenario->setLog($thermostat_ecs == 0 ? "ECS OFF - Thermostat satisfait (T={$temp_ecs}C)" : "ECS OFF - Mode auto désactivé"); } elseif ($gridc_ecs_forcee == 'OFF' || ($gridc_ecs_reac_since > 0 && ($now - $gridc_ecs_reac_since) < $timeout_ecs_reactivation)) { relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisOFF($etatR3, $ID_cmdR3_OFF, $force_resync); $etatR3 = false; if ($gridc_ecs_forcee == 'OFF') { $scenario->setLog("ECS OFF - Conso Réseau non couvrable (ajust-C actif)"); } else { $reste_reac = $timeout_ecs_reactivation - ($now - $gridc_ecs_reac_since); $scenario->setLog("ECS OFF - timer réactivation post-[C] ({$reste_reac}s restantes)"); } } elseif ($gridc_ecs_forcee == 'R3') { relaisOFF($etatR1, $ID_cmdR1_OFF, true); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, true); $etatR2 = false; relaisON($etatR3, $ID_cmdR3_ON, true); $etatR3 = true; $palier_ecs_actif = $conso_ecs; $surplus_pour_bat = $surplus_total - $conso_ecs; $scenario->setLog("⚡ ECS R3 forcée (Conso Réseau) {$conso_ecs}W | Bat←{$surplus_pour_bat}W"); } elseif ($gridc_ecs_forcee == 'R2') { relaisOFF($etatR1, $ID_cmdR1_OFF, true); $etatR1 = false; relaisON($etatR2, $ID_cmdR2_ON, true); $etatR2 = true; relaisOFF($etatR3, $ID_cmdR3_OFF, true); $etatR3 = false; $palier_ecs_actif = $conso_ecs; $surplus_pour_bat = $surplus_total - $conso_ecs; $scenario->setLog("⚡ ECS R2 forcée (Conso Réseau) {$conso_ecs}W | Bat←{$surplus_pour_bat}W"); } elseif ($surplus_total >= $seuil_R1_on) { relaisON($etatR1, $ID_cmdR1_ON, $force_resync); $etatR1 = true; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisOFF($etatR3, $ID_cmdR3_OFF, $force_resync); $etatR3 = false; $palier_ecs_actif = $conso_ecs; $conso_ecs_effective = ($conso_ecs < 100) ? $puissance_R1 : $conso_ecs; $surplus_pour_bat = $surplus_total - $conso_ecs_effective; $tag = ($etatR1 && !$force_resync) ? "ℹ️[inchangé]" : "✅[changé]"; $msg = ($conso_ecs < 100) ? "démarrage (réservé {$puissance_R1}W)" : "{$conso_ecs}W"; $scenario->setLog("⚡ ECS R1 ON {$msg} | Bat←{$surplus_pour_bat}W {$tag}"); } elseif ($surplus_total >= $seuil_R2_on) { relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisON($etatR2, $ID_cmdR2_ON, $force_resync); $etatR2 = true; relaisOFF($etatR3, $ID_cmdR3_OFF, $force_resync); $etatR3 = false; $palier_ecs_actif = $conso_ecs; $conso_ecs_effective = ($conso_ecs < 100) ? $puissance_R2 : $conso_ecs; $surplus_pour_bat = $surplus_total - $conso_ecs_effective; $tag = ($etatR2 && !$force_resync) ? "ℹ️[inchangé]" : "✅[changé]"; $msg = ($conso_ecs < 100) ? "démarrage (réservé {$puissance_R2}W)" : "{$conso_ecs}W"; $scenario->setLog("⚡ ECS R2 ON {$msg} | Bat←{$surplus_pour_bat}W {$tag}"); } elseif ($surplus_total >= $seuil_R3_on) { relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisON($etatR3, $ID_cmdR3_ON, $force_resync); $etatR3 = true; $palier_ecs_actif = $conso_ecs; $conso_ecs_effective = ($conso_ecs < 100) ? $puissance_R3 : $conso_ecs; $surplus_pour_bat = $surplus_total - $conso_ecs_effective; $tag = ($etatR3 && !$force_resync) ? "ℹ️[inchangé]" : "✅[changé]"; $msg = ($conso_ecs < 100) ? "démarrage (réservé {$puissance_R3}W)" : "{$conso_ecs}W"; $scenario->setLog("⚡ ECS R3 ON {$msg} | Bat←{$surplus_pour_bat}W {$tag}"); } elseif ($surplus_total < $seuil_R3_off) { relaisOFF($etatR1, $ID_cmdR1_OFF, $force_resync); $etatR1 = false; relaisOFF($etatR2, $ID_cmdR2_OFF, $force_resync); $etatR2 = false; relaisOFF($etatR3, $ID_cmdR3_OFF, $force_resync); $etatR3 = false; $scenario->setLog("ECS OFF - Surplus={$surplus_total}W insuffisant"); } else { if ($etatR1) { $palier_ecs_actif = $conso_ecs; $surplus_pour_bat = $surplus_total - $conso_ecs; $scenario->setLog("⚡ ECS R1 maintenu {$conso_ecs}W (hystérésis) | Bat←{$surplus_pour_bat}W"); } elseif ($etatR2) { $palier_ecs_actif = $conso_ecs; $surplus_pour_bat = $surplus_total - $conso_ecs; $scenario->setLog("⚡ ECS R2 maintenu {$conso_ecs}W (hystérésis) | Bat←{$surplus_pour_bat}W"); } elseif ($etatR3) { $palier_ecs_actif = $conso_ecs; $surplus_pour_bat = $surplus_total - $conso_ecs; $scenario->setLog("⚡ ECS R3 maintenu {$conso_ecs}W (hystérésis) | Bat←{$surplus_pour_bat}W"); } else { $scenario->setLog("ECS OFF - hystérésis (surplus insuffisant)"); } } /* ======================================== ETAPE 6 : CHARGE BATTERIE (mode normal) ======================================== */ $bat_peut_charger = ( $soc < $soc_max_charge && $mode_bat != 3 && $surplus_pour_bat > $min_power && !$charge_interdite_temp ); if ($bat_peut_charger) { $bat_cible_final = (int) round(min($bat_max_charge, $surplus_pour_bat * $coef_charge_bat)); if ($bat_cible_final >= $min_power) { $changed = setBatCharge($bat_cible_final, $p_bat_charge, $mode_bat, $force_resync, $ID_mode_charge, $ID_puissance_charge, $ID_puissance_decharge); $tag = $changed ? "✅[CHANGE]" : "ℹ️[INCHANGE]"; $adj = ($gridc_bat_max_adj > 0) ? " [GridC={$gridc_bat_max_adj}W]" : ""; $scenario->setLog("⚡ Bat charge {$bat_cible_final}W (dispo={$surplus_pour_bat}W){$adj} {$tag}"); $etat_systeme = ($palier_ecs_actif > 0) ? 'Charge - Prio ECS' : 'Charge - Prio Batterie'; } else { stopBatCharge($p_bat_charge, $mode_bat, $ID_puissance_charge); $scenario->setLog("ℹ️ Surplus trop faible ({$bat_cible_final}W < {$min_power}W)"); $etat_systeme = 'Veille'; } } else { stopBatCharge($p_bat_charge, $mode_bat, $ID_puissance_charge); if ($mode_bat == 3) { $scenario->setLog("ℹ️ Batterie pleine (mode=3)"); $etat_systeme = ($palier_ecs_actif > 0) ? 'Charge - Prio ECS' : 'Veille'; } elseif ($soc >= $soc_max_charge) { $scenario->setLog("ℹ️ Batterie pleine - SOC max ({$soc}%)"); $etat_systeme = ($palier_ecs_actif > 0) ? 'Charge - Prio ECS' : 'Veille'; } elseif ($charge_interdite_temp) { $scenario->setLog("⚠️ Charge impossible - Température interdite ({$temp_bat}C)"); $etat_systeme = ($palier_ecs_actif > 0) ? 'Charge - Prio ECS' : 'Veille'; } elseif ($conso_reseau > 0 && $surplus_pour_bat <= $conso_reseau) { $scenario->setLog("ℹ️ Surplus insuffisant ({$surplus_pour_bat}W) face à Conso Réseau ({$conso_reseau}W)"); $etat_systeme = 'Veille'; } else { $scenario->setLog("ℹ️ Surplus trop faible ({$surplus_pour_bat}W)"); $etat_systeme = 'Veille'; } } circulateur: /* ======================================== CIRCULATEUR + TIMEOUT 15min MAX ======================================== */ $timestampCirc = (int) ($scenario->getData('circ_last_start') ?: 0); $ecs_relais_actif = ($etatR1 || $etatR2 || $etatR3); $ecs_coupee_gridc = ($gridc_ecs_forcee == 'OFF'); $force_off = false; if (!$ecs_relais_actif || $temp_ecs < $temp_ecs_min_circ || $ecs_coupee_gridc) { $force_off = true; } elseif ($timestampCirc && ($now - $timestampCirc) > $timeout_circ_max) { $force_off = true; $scenario->setLog("⏰ TIMEOUT circulateur atteint (T={$temp_ecs}C) - OFF"); } if ($force_off && $etatCirc) { cmd::byId($ID_cmdCircOFF)->execCmd(); $scenario->setData('circ_last_start', 0); $scenario->setLog(">> Circulateur OFF (timeout/thermo/ECS/GridC)"); $etatCirc = false; } elseif (!$force_off && !$etatCirc) { $anti_marteau_ok = !$timestampCirc || ($now - $timestampCirc) > $anti_marteau_delai; if ($anti_marteau_ok) { cmd::byId($ID_cmdCircON)->execCmd(); $scenario->setData('circ_last_start', $now); $relais_actifs = ($etatR1 ? "R1 " : "") . ($etatR2 ? "R2 " : "") . ($etatR3 ? "R3 " : ""); $duree = $timestampCirc ? " (anti-marteau {$anti_marteau_delai}s)" : ""; $scenario->setLog(">> Circulateur ON (ECS {$relais_actifs}T={$temp_ecs}C{$duree})"); $etatCirc = true; } } /* ======================================== RESUME FINAL ======================================== */ cmd::byId($ID_etat_systeme)->event($etat_systeme); $adj_status = ''; if ($gridc_ajuste) { if ($gridc_bat_max_adj > 0) $adj_status = " [GridC-A bat_max={$gridc_bat_max_adj}W]"; elseif ($gridc_ecs_forcee != '') $adj_status = " [GridC-B/C ECS={$gridc_ecs_forcee}]"; } $nuageux_status = $mode_nuageux ? " [⛅Nuageux]" : ($en_transition_nuageux ? " [☀️Transition]" : ""); $scenario->setLog("✅ SOC={$soc}% " . ($mode_bat_label[$mode_bat] ?? $mode_bat) . " | ECS={$palier_ecs_actif}W | Surplus={$surplus_total}W | Conso={$conso_reseau}W{$adj_status}{$nuageux_status} | État→{$etat_systeme}"); if ($urgence_active) { $scenario->setLog("[!!] URGENCE active | Recharge réseau {$p_recharge_urgence}W | cible {$soc_urgence_fin_recharge}%"); } elseif ($soc >= $soc_optimal_min && $soc <= $soc_optimal_max) { $scenario->setLog("✅ Zone optimale ({$soc_optimal_min}-{$soc_optimal_max}%)"); } elseif ($soc < $soc_optimal_min) { $scenario->setLog("⚠️ Batterie basse (<{$soc_optimal_min}%) - favoriser charge"); } else { $scenario->setLog("✅ Batterie haute (>{$soc_optimal_max}%) - décharge prioritaire"); } $scenario->setLog("===== FIN ==========");