From cbd6c4d30af266b9f4a44848e75d2bc33637bb1c Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sat, 20 Dec 2025 09:41:45 +0100 Subject: [PATCH 01/39] Create power_meter_shelly_em_pro --- solar_router/power_meter_shelly_em_pro | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 solar_router/power_meter_shelly_em_pro diff --git a/solar_router/power_meter_shelly_em_pro b/solar_router/power_meter_shelly_em_pro new file mode 100644 index 00000000..01cfaa4e --- /dev/null +++ b/solar_router/power_meter_shelly_em_pro @@ -0,0 +1,83 @@ +<<: !include power_meter_common.yaml + +esphome: + min_version: 2025.5.0 + +# ---------------------------------------------------------------------------------------------------- +# Use http request component +# ---------------------------------------------------------------------------------------------------- + +http_request: + id: http_request_data + useragent: esphome/device + timeout: 10s + verify_ssl: False + +# ---------------------------------------------------------------------------------------------------- +# Define scripts for power collection +# ---------------------------------------------------------------------------------------------------- + +script: + # Shelly script gather power reports from Energy Meter and update globals (real_power) + # act_power is transformed to power to keep compatibility + - id: power_meter_source + mode: single + then: + - if: + condition: + lambda: 'return network::is_connected();' + then: + - http_request.get: + url: http://${power_meter_ip_address}/rpc/Shelly.GetStatus + request_headers: + Content-Type: application/json + Authorization: ${power_meter_auth_header} + capture_response: true + max_response_buffer_size: 4096 + on_response: + then: + - logger.log: + format: 'Response status: %d, Duration: %u ms' + args: + - response->status_code + - response->duration_ms + - lambda: |- + if (response->status_code != 200) { + ESP_LOGW("custom", "HTTP Request failed with status: %d", response->status_code); + id(real_power).publish_state(NAN); + } else { + + bool parse_success = json::parse_json(body, [](JsonObject root) -> bool { + // Shelly EM Pro 50 -> EM1.act_power + if (!root["emeters"][${emeter_index}]["act_power"].is()) { + ESP_LOGW("custom", "act_power not found"); + return false; + } + + // Map act_power -> power (compatibility) + float power = root["emeters"][${emeter_index}]["act_power"].as(); + id(real_power).publish_state(power); + return true; + }); + + if (!parse_success) { + ESP_LOGW("custom", "JSON Parsing failed"); + id(real_power).publish_state(NAN); + } + } + on_error: + then: + - lambda: |- + ESP_LOGW("custom", "HTTP Request failed or timeout occurred"); + id(real_power).publish_state(NAN); + +time: + - platform: sntp + on_time: + - seconds: /1 + then: + - if: + condition: + - lambda: return id(power_meter_activated) != 0; + then: + - script.execute: power_meter_source From 6c43c82290740ffd35f9691f411a8d42a65cbc0a Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sat, 20 Dec 2025 09:50:58 +0100 Subject: [PATCH 02/39] Rename power_meter_shelly_em_pro to power_meter_shelly_em_pro.yaml --- .../{power_meter_shelly_em_pro => power_meter_shelly_em_pro.yaml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename solar_router/{power_meter_shelly_em_pro => power_meter_shelly_em_pro.yaml} (100%) diff --git a/solar_router/power_meter_shelly_em_pro b/solar_router/power_meter_shelly_em_pro.yaml similarity index 100% rename from solar_router/power_meter_shelly_em_pro rename to solar_router/power_meter_shelly_em_pro.yaml From f1b421b7a4b4d07f86bbe2af12c70316599a1cfa Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sat, 20 Dec 2025 09:57:55 +0100 Subject: [PATCH 03/39] Update power_meter_shelly_em_pro.yaml --- solar_router/power_meter_shelly_em_pro.yaml | 65 +++++++++++---------- 1 file changed, 34 insertions(+), 31 deletions(-) diff --git a/solar_router/power_meter_shelly_em_pro.yaml b/solar_router/power_meter_shelly_em_pro.yaml index 01cfaa4e..e3a0b73f 100644 --- a/solar_router/power_meter_shelly_em_pro.yaml +++ b/solar_router/power_meter_shelly_em_pro.yaml @@ -2,30 +2,26 @@ esphome: min_version: 2025.5.0 - + # ---------------------------------------------------------------------------------------------------- -# Use http request component +# HTTP client # ---------------------------------------------------------------------------------------------------- - http_request: id: http_request_data useragent: esphome/device timeout: 10s - verify_ssl: False + verify_ssl: false # ---------------------------------------------------------------------------------------------------- -# Define scripts for power collection +# Power meter script (Shelly EM Pro) # ---------------------------------------------------------------------------------------------------- - script: - # Shelly script gather power reports from Energy Meter and update globals (real_power) - # act_power is transformed to power to keep compatibility - id: power_meter_source mode: single then: - if: condition: - lambda: 'return network::is_connected();' + lambda: return network::is_connected(); then: - http_request.get: url: http://${power_meter_ip_address}/rpc/Shelly.GetStatus @@ -34,43 +30,50 @@ script: Authorization: ${power_meter_auth_header} capture_response: true max_response_buffer_size: 4096 + on_response: then: - logger.log: - format: 'Response status: %d, Duration: %u ms' + format: "Shelly HTTP %d (%u ms)" args: - response->status_code - response->duration_ms + - lambda: |- if (response->status_code != 200) { - ESP_LOGW("custom", "HTTP Request failed with status: %d", response->status_code); - id(real_power).publish_state(NAN); - } else { - - bool parse_success = json::parse_json(body, [](JsonObject root) -> bool { - // Shelly EM Pro 50 -> EM1.act_power - if (!root["emeters"][${emeter_index}]["act_power"].is()) { - ESP_LOGW("custom", "act_power not found"); - return false; - } + ESP_LOGW("shelly", "HTTP error %d", response->status_code); + id(real_power).publish_state(NAN); + return; + } - // Map act_power -> power (compatibility) - float power = root["emeters"][${emeter_index}]["act_power"].as(); - id(real_power).publish_state(power); - return true; - }); + bool ok = json::parse_json(body, [&](JsonObject root) -> bool { + int index = ${emeter_index}; + String key = "em1:" + String(index); - if (!parse_success) { - ESP_LOGW("custom", "JSON Parsing failed"); - id(real_power).publish_state(NAN); + if (!root[key.c_str()]["act_power"].is()) { + ESP_LOGW("shelly", "act_power not found for %s", key.c_str()); + return false; } + + float power = root[key.c_str()]["act_power"].as(); + id(real_power).publish_state(power); + return true; + }); + + if (!ok) { + ESP_LOGW("shelly", "JSON parse failed"); + id(real_power).publish_state(NAN); } + on_error: then: - lambda: |- - ESP_LOGW("custom", "HTTP Request failed or timeout occurred"); - id(real_power).publish_state(NAN); + ESP_LOGW("shelly", "HTTP request failed"); + id(real_power).publish_state(NAN) +# ---------------------------------------------------------------------------------------------------- +# Periodic update +# ---------------------------------------------------------------------------------------------------- time: - platform: sntp on_time: @@ -78,6 +81,6 @@ time: then: - if: condition: - - lambda: return id(power_meter_activated) != 0; + lambda: return id(power_meter_activated) != 0; then: - script.execute: power_meter_source From 7d731f4e3a4091ed6a272f59498e0ef137581d05 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sat, 20 Dec 2025 10:01:10 +0100 Subject: [PATCH 04/39] Update power_meter_shelly_em_pro.yaml --- solar_router/power_meter_shelly_em_pro.yaml | 32 ++++++--------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/solar_router/power_meter_shelly_em_pro.yaml b/solar_router/power_meter_shelly_em_pro.yaml index e3a0b73f..7bf68720 100644 --- a/solar_router/power_meter_shelly_em_pro.yaml +++ b/solar_router/power_meter_shelly_em_pro.yaml @@ -3,18 +3,12 @@ esphome: min_version: 2025.5.0 -# ---------------------------------------------------------------------------------------------------- -# HTTP client -# ---------------------------------------------------------------------------------------------------- http_request: id: http_request_data useragent: esphome/device timeout: 10s verify_ssl: false -# ---------------------------------------------------------------------------------------------------- -# Power meter script (Shelly EM Pro) -# ---------------------------------------------------------------------------------------------------- script: - id: power_meter_source mode: single @@ -30,15 +24,8 @@ script: Authorization: ${power_meter_auth_header} capture_response: true max_response_buffer_size: 4096 - on_response: then: - - logger.log: - format: "Shelly HTTP %d (%u ms)" - args: - - response->status_code - - response->duration_ms - - lambda: |- if (response->status_code != 200) { ESP_LOGW("shelly", "HTTP error %d", response->status_code); @@ -46,22 +33,22 @@ script: return; } - bool ok = json::parse_json(body, [&](JsonObject root) -> bool { - int index = ${emeter_index}; - String key = "em1:" + String(index); + bool ok = json::parse_json(body, [](JsonObject root) -> bool { + // em1:0 / em1:1 + char key[8]; + snprintf(key, sizeof(key), "em1:%d", ${emeter_index}); - if (!root[key.c_str()]["act_power"].is()) { - ESP_LOGW("shelly", "act_power not found for %s", key.c_str()); + if (!root[key]["act_power"].is()) { + ESP_LOGW("shelly", "act_power missing for %s", key); return false; } - float power = root[key.c_str()]["act_power"].as(); + float power = root[key]["act_power"].as(); id(real_power).publish_state(power); return true; }); if (!ok) { - ESP_LOGW("shelly", "JSON parse failed"); id(real_power).publish_state(NAN); } @@ -69,11 +56,8 @@ script: then: - lambda: |- ESP_LOGW("shelly", "HTTP request failed"); - id(real_power).publish_state(NAN) + id(real_power).publish_state(NAN); -# ---------------------------------------------------------------------------------------------------- -# Periodic update -# ---------------------------------------------------------------------------------------------------- time: - platform: sntp on_time: From ddbc8a8de14069b1b311e53e0b0b261e182c5e25 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sat, 20 Dec 2025 10:07:07 +0100 Subject: [PATCH 05/39] Update power_meter_shelly_em_pro.yaml --- solar_router/power_meter_shelly_em_pro.yaml | 31 ++++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/solar_router/power_meter_shelly_em_pro.yaml b/solar_router/power_meter_shelly_em_pro.yaml index 7bf68720..5131116a 100644 --- a/solar_router/power_meter_shelly_em_pro.yaml +++ b/solar_router/power_meter_shelly_em_pro.yaml @@ -3,12 +3,19 @@ esphome: min_version: 2025.5.0 +# ---------------------------------------------------------------------------------------------------- +# HTTP client +# ---------------------------------------------------------------------------------------------------- http_request: id: http_request_data useragent: esphome/device timeout: 10s verify_ssl: false +# ---------------------------------------------------------------------------------------------------- +# Power meter script – Shelly EM Pro +# act_power -> real_power (compatibility with existing logic) +# ---------------------------------------------------------------------------------------------------- script: - id: power_meter_source mode: single @@ -24,6 +31,7 @@ script: Authorization: ${power_meter_auth_header} capture_response: true max_response_buffer_size: 4096 + on_response: then: - lambda: |- @@ -34,21 +42,27 @@ script: } bool ok = json::parse_json(body, [](JsonObject root) -> bool { - // em1:0 / em1:1 - char key[8]; - snprintf(key, sizeof(key), "em1:%d", ${emeter_index}); - if (!root[key]["act_power"].is()) { - ESP_LOGW("shelly", "act_power missing for %s", key); + // ---- SELECT EMETER ---- + JsonObject em; + if (${emeter_index} == 0) { + em = root["em1:0"]; + } else { + em = root["em1:1"]; + } + + if (!em["act_power"].is()) { + ESP_LOGW("shelly", "act_power missing"); return false; } - float power = root[key]["act_power"].as(); + float power = em["act_power"].as(); id(real_power).publish_state(power); return true; }); if (!ok) { + ESP_LOGW("shelly", "JSON parse failed"); id(real_power).publish_state(NAN); } @@ -56,8 +70,11 @@ script: then: - lambda: |- ESP_LOGW("shelly", "HTTP request failed"); - id(real_power).publish_state(NAN); + id(real_power).publish_state(NAN) +# ---------------------------------------------------------------------------------------------------- +# Periodic execution +# ---------------------------------------------------------------------------------------------------- time: - platform: sntp on_time: From 96bfdb4631cda28288d9b0d5c1c67f1fdc4a2820 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sat, 20 Dec 2025 10:10:37 +0100 Subject: [PATCH 06/39] Update power_meter_shelly_em_pro.yaml --- solar_router/power_meter_shelly_em_pro.yaml | 31 ++++----------------- 1 file changed, 6 insertions(+), 25 deletions(-) diff --git a/solar_router/power_meter_shelly_em_pro.yaml b/solar_router/power_meter_shelly_em_pro.yaml index 5131116a..c1e0c420 100644 --- a/solar_router/power_meter_shelly_em_pro.yaml +++ b/solar_router/power_meter_shelly_em_pro.yaml @@ -3,19 +3,12 @@ esphome: min_version: 2025.5.0 -# ---------------------------------------------------------------------------------------------------- -# HTTP client -# ---------------------------------------------------------------------------------------------------- http_request: id: http_request_data useragent: esphome/device timeout: 10s verify_ssl: false -# ---------------------------------------------------------------------------------------------------- -# Power meter script – Shelly EM Pro -# act_power -> real_power (compatibility with existing logic) -# ---------------------------------------------------------------------------------------------------- script: - id: power_meter_source mode: single @@ -31,7 +24,6 @@ script: Authorization: ${power_meter_auth_header} capture_response: true max_response_buffer_size: 4096 - on_response: then: - lambda: |- @@ -42,39 +34,28 @@ script: } bool ok = json::parse_json(body, [](JsonObject root) -> bool { + char key[8]; + snprintf(key, sizeof(key), "em1:%d", ${emeter_index}); - // ---- SELECT EMETER ---- - JsonObject em; - if (${emeter_index} == 0) { - em = root["em1:0"]; - } else { - em = root["em1:1"]; - } - - if (!em["act_power"].is()) { - ESP_LOGW("shelly", "act_power missing"); + if (!root[key]["act_power"].is()) { + ESP_LOGW("shelly", "act_power missing for %s", key); return false; } - float power = em["act_power"].as(); + float power = root[key]["act_power"].as(); id(real_power).publish_state(power); return true; }); if (!ok) { - ESP_LOGW("shelly", "JSON parse failed"); id(real_power).publish_state(NAN); } - on_error: then: - lambda: |- ESP_LOGW("shelly", "HTTP request failed"); - id(real_power).publish_state(NAN) + id(real_power).publish_state(NAN); -# ---------------------------------------------------------------------------------------------------- -# Periodic execution -# ---------------------------------------------------------------------------------------------------- time: - platform: sntp on_time: From db9f5fd08a20f89ab6bc3120160613230e394876 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 07:54:35 +0100 Subject: [PATCH 07/39] Create scheduler_forced_run_safe.yaml The scheduler was updated to persist all time and threshold settings across reboots using restore_value. It now evaluates its state on boot and immediately applies actions if the current time is already within the scheduled window. A safety check was added so temperature limits override the scheduler instantly, stopping the router when exceeded. Overall, the scheduler is now reboot-safe, state-consistent, and safety-aware. --- solar_router/scheduler_forced_run_safe.yaml | 152 ++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 solar_router/scheduler_forced_run_safe.yaml diff --git a/solar_router/scheduler_forced_run_safe.yaml b/solar_router/scheduler_forced_run_safe.yaml new file mode 100644 index 00000000..f1a72f9c --- /dev/null +++ b/solar_router/scheduler_forced_run_safe.yaml @@ -0,0 +1,152 @@ +#The scheduler was updated to persist all time and threshold settings across reboots using restore_value. +#It now evaluates its state on boot and immediately applies actions if the current time is already within the scheduled window. +#A safety check was added so temperature limits override the scheduler instantly, stopping the router when exceeded. +#Overall, the scheduler is now reboot-safe, state-consistent, and safety-aware. +substitutions: + scheduler_unique_id: "Forced" + custom_script: ${scheduler_unique_id}_fake_script + +esphome: + on_boot: + priority: -100 + then: + - wait_until: + lambda: return id(${scheduler_unique_id}_sntp).now().is_valid(); + - script.execute: ${scheduler_unique_id}_scheduler_check_now + +script: + # Script vide par défaut + - id: ${scheduler_unique_id}_fake_script + then: + + # Script central : applique l’état correct immédiatement + - id: ${scheduler_unique_id}_scheduler_check_now + mode: restart + then: + - lambda: |- + if (!id(${scheduler_unique_id}_scheduler_activate).state) { + return; + } + + // Sécurité température prioritaire + if (id(safety_limit)) { + id(router_level).publish_state(0); + id(activate).turn_on(); + return; + } + + int beginTotalMinutes = + id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + + int endTotalMinutes = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + + int nowTotalMinutes = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + + bool in_range; + if (beginTotalMinutes <= endTotalMinutes) { + in_range = nowTotalMinutes >= beginTotalMinutes && + nowTotalMinutes < endTotalMinutes; + } else { + in_range = nowTotalMinutes >= beginTotalMinutes || + nowTotalMinutes < endTotalMinutes; + } + + if (in_range) { + id(activate).turn_off(); + id(router_level).publish_state( + id(${scheduler_unique_id}_scheduler_router_level).state + ); + id(${custom_script}).execute(); + } else { + id(router_level).publish_state(0); + id(activate).turn_on(); + } + +switch: + - platform: template + name: "Activate ${scheduler_unique_id} Scheduler" + id: ${scheduler_unique_id}_scheduler_activate + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + on_turn_on: + then: + - script.execute: ${scheduler_unique_id}_scheduler_check_now + on_turn_off: + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate + +number: + - platform: template + name: "${scheduler_unique_id} Scheduler Router Level" + id: ${scheduler_unique_id}_scheduler_router_level + min_value: 0 + max_value: 100 + step: 1 + optimistic: true + restore_value: true + initial_value: 100 + unit_of_measurement: "%" + + - platform: template + name: "${scheduler_unique_id} Scheduler Checking End Threshold" + id: ${scheduler_unique_id}_scheduler_checking_end_threshold + min_value: 0 + max_value: 720 + step: 5 + optimistic: true + restore_value: true + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Hour" + id: ${scheduler_unique_id}_scheduler_begin_hour + min_value: 0 + max_value: 23 + step: 1 + optimistic: true + restore_value: true + initial_value: 0 + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: ${scheduler_unique_id}_scheduler_begin_min + min_value: 0 + max_value: 55 + step: 5 + optimistic: true + restore_value: true + + - platform: template + name: "${scheduler_unique_id} Scheduler End Hour" + id: ${scheduler_unique_id}_scheduler_end_hour + min_value: 0 + max_value: 23 + step: 1 + optimistic: true + restore_value: true + initial_value: 2 + + - platform: template + name: "${scheduler_unique_id} Scheduler End Minute" + id: ${scheduler_unique_id}_scheduler_end_min + min_value: 0 + max_value: 55 + step: 5 + optimistic: true + restore_value: true + +time: + - platform: sntp + id: ${scheduler_unique_id}_sntp + on_time: + - seconds: 0 + minutes: /1 + then: + - script.execute: ${scheduler_unique_id}_scheduler_check_now From 158476d6d2df6745f8cef3f28611f891d525ba0a Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 08:07:31 +0100 Subject: [PATCH 08/39] Update scheduler_forced_run_safe.yaml correct mode: box missing in hour and minutes definition --- solar_router/scheduler_forced_run_safe.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/solar_router/scheduler_forced_run_safe.yaml b/solar_router/scheduler_forced_run_safe.yaml index f1a72f9c..58b9b03f 100644 --- a/solar_router/scheduler_forced_run_safe.yaml +++ b/solar_router/scheduler_forced_run_safe.yaml @@ -101,6 +101,7 @@ number: min_value: 0 max_value: 720 step: 5 + mode: box optimistic: true restore_value: true @@ -110,6 +111,7 @@ number: min_value: 0 max_value: 23 step: 1 + mode: box optimistic: true restore_value: true initial_value: 0 @@ -120,6 +122,7 @@ number: min_value: 0 max_value: 55 step: 5 + mode: box optimistic: true restore_value: true @@ -129,6 +132,7 @@ number: min_value: 0 max_value: 23 step: 1 + mode: box optimistic: true restore_value: true initial_value: 2 @@ -139,6 +143,7 @@ number: min_value: 0 max_value: 55 step: 5 + mode: box optimistic: true restore_value: true From b25b59ad101ed1b234453d2914131594ddd035cc Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 08:26:30 +0100 Subject: [PATCH 09/39] Update scheduler_forced_run_safe.yaml --- solar_router/scheduler_forced_run_safe.yaml | 165 +++++++++----------- 1 file changed, 74 insertions(+), 91 deletions(-) diff --git a/solar_router/scheduler_forced_run_safe.yaml b/solar_router/scheduler_forced_run_safe.yaml index 58b9b03f..39c003f3 100644 --- a/solar_router/scheduler_forced_run_safe.yaml +++ b/solar_router/scheduler_forced_run_safe.yaml @@ -1,71 +1,64 @@ -#The scheduler was updated to persist all time and threshold settings across reboots using restore_value. -#It now evaluates its state on boot and immediately applies actions if the current time is already within the scheduled window. -#A safety check was added so temperature limits override the scheduler instantly, stopping the router when exceeded. -#Overall, the scheduler is now reboot-safe, state-consistent, and safety-aware. +# Differences from original scheduler: +# - Scheduler parameters (hours, minutes, threshold) are now persistent across reboots +# - Scheduler actions are applied immediately on boot if current time is inside the window +# - Safety limit is checked before forcing router activation +# - Uses numeric inputs (box) instead of sliders to prevent ESP crashes substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script esphome: on_boot: - priority: -100 + priority: -100.0 then: - - wait_until: - lambda: return id(${scheduler_unique_id}_sntp).now().is_valid(); - - script.execute: ${scheduler_unique_id}_scheduler_check_now + - script.execute: ${scheduler_unique_id}_scheduler_evaluate script: - # Script vide par défaut + # Fake empty script if user does not provide one - id: ${scheduler_unique_id}_fake_script then: - # Script central : applique l’état correct immédiatement - - id: ${scheduler_unique_id}_scheduler_check_now + # Central scheduler evaluation (used on boot + every 5 minutes) + - id: ${scheduler_unique_id}_scheduler_evaluate mode: restart then: - - lambda: |- - if (!id(${scheduler_unique_id}_scheduler_activate).state) { - return; - } - - // Sécurité température prioritaire - if (id(safety_limit)) { - id(router_level).publish_state(0); - id(activate).turn_on(); - return; - } - - int beginTotalMinutes = - id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_begin_min).state; - - int endTotalMinutes = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - - int nowTotalMinutes = - id(${scheduler_unique_id}_sntp).now().hour * 60 + - id(${scheduler_unique_id}_sntp).now().minute; - - bool in_range; - if (beginTotalMinutes <= endTotalMinutes) { - in_range = nowTotalMinutes >= beginTotalMinutes && - nowTotalMinutes < endTotalMinutes; - } else { - in_range = nowTotalMinutes >= beginTotalMinutes || - nowTotalMinutes < endTotalMinutes; - } - - if (in_range) { - id(activate).turn_off(); - id(router_level).publish_state( - id(${scheduler_unique_id}_scheduler_router_level).state - ); - id(${custom_script}).execute(); - } else { - id(router_level).publish_state(0); - id(activate).turn_on(); - } + - if: + condition: + - switch.is_on: ${scheduler_unique_id}_scheduler_activate + - lambda: |- + int beginTotal = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + int endTotal = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int nowTotal = id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + + if (beginTotal <= endTotal) { + return nowTotal >= beginTotal && nowTotal < endTotal; + } else { + return nowTotal >= beginTotal || nowTotal < endTotal; + } + then: + # Inside scheduler window + - if: + condition: + lambda: return !id(safety_limit); + then: + - switch.turn_off: activate + - number.set: + id: router_level + value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; + - script.execute: ${custom_script} + else: + # Outside scheduler window → restore router + - if: + condition: + - switch.is_off: activate + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate switch: - platform: template @@ -73,15 +66,6 @@ switch: id: ${scheduler_unique_id}_scheduler_activate optimistic: true restore_mode: RESTORE_DEFAULT_ON - on_turn_on: - then: - - script.execute: ${scheduler_unique_id}_scheduler_check_now - on_turn_off: - then: - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate number: - platform: template @@ -90,9 +74,10 @@ number: min_value: 0 max_value: 100 step: 1 - optimistic: true - restore_value: true initial_value: 100 + restore_value: true + optimistic: true + mode: box unit_of_measurement: "%" - platform: template @@ -101,9 +86,29 @@ number: min_value: 0 max_value: 720 step: 5 + restore_value: true + optimistic: true mode: box + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: ${scheduler_unique_id}_scheduler_begin_min + min_value: 0 + max_value: 59 + step: 1 + restore_value: true optimistic: true + mode: box + + - platform: template + name: "${scheduler_unique_id} Scheduler End Minute" + id: ${scheduler_unique_id}_scheduler_end_min + min_value: 0 + max_value: 59 + step: 1 restore_value: true + optimistic: true + mode: box - platform: template name: "${scheduler_unique_id} Scheduler Begin Hour" @@ -111,20 +116,9 @@ number: min_value: 0 max_value: 23 step: 1 - mode: box - optimistic: true restore_value: true - initial_value: 0 - - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Minute" - id: ${scheduler_unique_id}_scheduler_begin_min - min_value: 0 - max_value: 55 - step: 5 - mode: box optimistic: true - restore_value: true + mode: box - platform: template name: "${scheduler_unique_id} Scheduler End Hour" @@ -132,26 +126,15 @@ number: min_value: 0 max_value: 23 step: 1 - mode: box - optimistic: true restore_value: true - initial_value: 2 - - - platform: template - name: "${scheduler_unique_id} Scheduler End Minute" - id: ${scheduler_unique_id}_scheduler_end_min - min_value: 0 - max_value: 55 - step: 5 - mode: box optimistic: true - restore_value: true + mode: box time: - platform: sntp id: ${scheduler_unique_id}_sntp on_time: - seconds: 0 - minutes: /1 + minutes: /5 then: - - script.execute: ${scheduler_unique_id}_scheduler_check_now + - script.execute: ${scheduler_unique_id}_scheduler_evaluate From e55656a247cf510e107fe27fb2cb032a348dc327 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 08:47:47 +0100 Subject: [PATCH 10/39] Update scheduler_forced_run_safe.yaml --- solar_router/scheduler_forced_run_safe.yaml | 161 +++++++++----------- 1 file changed, 76 insertions(+), 85 deletions(-) diff --git a/solar_router/scheduler_forced_run_safe.yaml b/solar_router/scheduler_forced_run_safe.yaml index 39c003f3..77a3d951 100644 --- a/solar_router/scheduler_forced_run_safe.yaml +++ b/solar_router/scheduler_forced_run_safe.yaml @@ -1,64 +1,57 @@ -# Differences from original scheduler: -# - Scheduler parameters (hours, minutes, threshold) are now persistent across reboots -# - Scheduler actions are applied immediately on boot if current time is inside the window -# - Safety limit is checked before forcing router activation -# - Uses numeric inputs (box) instead of sliders to prevent ESP crashes +# Changes vs original scheduler: +# 1) All scheduler parameters are now persistent across reboots (restore_value). +# 2) Scheduler state is evaluated once after SNTP sync to apply immediately if inside the time window. +# 3) Temperature safety (safety_limit) has absolute priority and stops routing instantly. +# 4) UI remains identical to original (number/box, no sliders) to avoid ESP crashes. substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script -esphome: - on_boot: - priority: -100.0 - then: - - script.execute: ${scheduler_unique_id}_scheduler_evaluate - script: - # Fake empty script if user does not provide one + # Empty script if user does not define one - id: ${scheduler_unique_id}_fake_script then: - # Central scheduler evaluation (used on boot + every 5 minutes) - - id: ${scheduler_unique_id}_scheduler_evaluate + # Central evaluation script (safe to call) + - id: ${scheduler_unique_id}_evaluate mode: restart then: - - if: - condition: - - switch.is_on: ${scheduler_unique_id}_scheduler_activate - - lambda: |- - int beginTotal = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 - + id(${scheduler_unique_id}_scheduler_begin_min).state; - int endTotal = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 - + id(${scheduler_unique_id}_scheduler_end_min).state; - int nowTotal = id(${scheduler_unique_id}_sntp).now().hour * 60 - + id(${scheduler_unique_id}_sntp).now().minute; + - lambda: |- + // Do nothing until time is valid + if (!id(${scheduler_unique_id}_sntp).now().is_valid()) { + return; + } - if (beginTotal <= endTotal) { - return nowTotal >= beginTotal && nowTotal < endTotal; - } else { - return nowTotal >= beginTotal || nowTotal < endTotal; - } - then: - # Inside scheduler window - - if: - condition: - lambda: return !id(safety_limit); - then: - - switch.turn_off: activate - - number.set: - id: router_level - value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; - - script.execute: ${custom_script} - else: - # Outside scheduler window → restore router - - if: - condition: - - switch.is_off: activate - then: - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate + // Safety has absolute priority + if (id(safety_limit)) { + id(router_level).publish_state(0); + id(activate).turn_on(); + return; + } + + int beginMin = (int) id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + (int) id(${scheduler_unique_id}_scheduler_begin_min).state; + int endMin = (int) id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + (int) id(${scheduler_unique_id}_scheduler_end_min).state; + int nowMin = id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + + bool in_window; + if (beginMin <= endMin) { + in_window = (nowMin >= beginMin && nowMin < endMin); + } else { + in_window = (nowMin >= beginMin || nowMin < endMin); + } + + if (id(${scheduler_unique_id}_scheduler_activate).state && in_window) { + id(activate).turn_off(); + id(router_level).publish_state( + id(${scheduler_unique_id}_scheduler_router_level).state + ); + } else { + id(router_level).publish_state(0); + id(activate).turn_on(); + } switch: - platform: template @@ -74,51 +67,31 @@ number: min_value: 0 max_value: 100 step: 1 - initial_value: 100 - restore_value: true - optimistic: true mode: box - unit_of_measurement: "%" + optimistic: true + restore_value: true + initial_value: 100 - platform: template - name: "${scheduler_unique_id} Scheduler Checking End Threshold" - id: ${scheduler_unique_id}_scheduler_checking_end_threshold + name: "${scheduler_unique_id} Scheduler Begin Hour" + id: ${scheduler_unique_id}_scheduler_begin_hour min_value: 0 - max_value: 720 - step: 5 - restore_value: true - optimistic: true + max_value: 23 + step: 1 mode: box + optimistic: true + restore_value: true + initial_value: 0 - platform: template name: "${scheduler_unique_id} Scheduler Begin Minute" id: ${scheduler_unique_id}_scheduler_begin_min min_value: 0 - max_value: 59 - step: 1 - restore_value: true - optimistic: true + max_value: 55 + step: 5 mode: box - - - platform: template - name: "${scheduler_unique_id} Scheduler End Minute" - id: ${scheduler_unique_id}_scheduler_end_min - min_value: 0 - max_value: 59 - step: 1 - restore_value: true optimistic: true - mode: box - - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Hour" - id: ${scheduler_unique_id}_scheduler_begin_hour - min_value: 0 - max_value: 23 - step: 1 restore_value: true - optimistic: true - mode: box - platform: template name: "${scheduler_unique_id} Scheduler End Hour" @@ -126,15 +99,33 @@ number: min_value: 0 max_value: 23 step: 1 - restore_value: true + mode: box optimistic: true + restore_value: true + initial_value: 2 + + - platform: template + name: "${scheduler_unique_id} Scheduler End Minute" + id: ${scheduler_unique_id}_scheduler_end_min + min_value: 0 + max_value: 55 + step: 5 mode: box + optimistic: true + restore_value: true time: - platform: sntp id: ${scheduler_unique_id}_sntp on_time: - seconds: 0 - minutes: /5 + minutes: /1 then: - - script.execute: ${scheduler_unique_id}_scheduler_evaluate + - script.execute: ${scheduler_unique_id}_evaluate + +esphome: + on_boot: + priority: -100 + then: + - delay: 10s + - script.execute: ${scheduler_unique_id}_evaluate From 210e700f2eedbb4332f744034d5c5db29c0ea9dd Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 17:01:37 +0100 Subject: [PATCH 11/39] Update scheduler_forced_run_safe.yaml custom tempo 22h - 6h --- solar_router/scheduler_forced_run_safe.yaml | 188 +++++++++++--------- 1 file changed, 103 insertions(+), 85 deletions(-) diff --git a/solar_router/scheduler_forced_run_safe.yaml b/solar_router/scheduler_forced_run_safe.yaml index 77a3d951..8a66eae0 100644 --- a/solar_router/scheduler_forced_run_safe.yaml +++ b/solar_router/scheduler_forced_run_safe.yaml @@ -1,131 +1,149 @@ -# Changes vs original scheduler: -# 1) All scheduler parameters are now persistent across reboots (restore_value). -# 2) Scheduler state is evaluated once after SNTP sync to apply immediately if inside the time window. -# 3) Temperature safety (safety_limit) has absolute priority and stops routing instantly. -# 4) UI remains identical to original (number/box, no sliders) to avoid ESP crashes. substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script script: - # Empty script if user does not define one + # A fake empty script to run if user don't provide a custom one - id: ${scheduler_unique_id}_fake_script then: - # Central evaluation script (safe to call) - - id: ${scheduler_unique_id}_evaluate - mode: restart - then: - - lambda: |- - // Do nothing until time is valid - if (!id(${scheduler_unique_id}_sntp).now().is_valid()) { - return; - } - - // Safety has absolute priority - if (id(safety_limit)) { - id(router_level).publish_state(0); - id(activate).turn_on(); - return; - } - - int beginMin = (int) id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 - + (int) id(${scheduler_unique_id}_scheduler_begin_min).state; - int endMin = (int) id(${scheduler_unique_id}_scheduler_end_hour).state * 60 - + (int) id(${scheduler_unique_id}_scheduler_end_min).state; - int nowMin = id(${scheduler_unique_id}_sntp).now().hour * 60 - + id(${scheduler_unique_id}_sntp).now().minute; - - bool in_window; - if (beginMin <= endMin) { - in_window = (nowMin >= beginMin && nowMin < endMin); - } else { - in_window = (nowMin >= beginMin || nowMin < endMin); - } - - if (id(${scheduler_unique_id}_scheduler_activate).state && in_window) { - id(activate).turn_off(); - id(router_level).publish_state( - id(${scheduler_unique_id}_scheduler_router_level).state - ); - } else { - id(router_level).publish_state(0); - id(activate).turn_on(); - } - switch: + # Define is scheduler is active or not - platform: template name: "Activate ${scheduler_unique_id} Scheduler" - id: ${scheduler_unique_id}_scheduler_activate optimistic: true restore_mode: RESTORE_DEFAULT_ON + id: "${scheduler_unique_id}_scheduler_activate" + on_turn_off: + then: + - if: + condition: + - switch.is_off: activate + then: + # Set router level to 0 then Enable router + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate number: + # Scheduler Router level from 0 to 100 + # When solar routing is disabled at scheduled time: Acts as a manual control to set the router level - platform: template name: "${scheduler_unique_id} Scheduler Router Level" - id: ${scheduler_unique_id}_scheduler_router_level + id: "${scheduler_unique_id}_scheduler_router_level" min_value: 0 max_value: 100 + initial_value: 100 step: 1 + unit_of_measurement: "%" + optimistic: True + mode: slider + - platform: template + name: "${scheduler_unique_id} Scheduler Checking End Threshold" + id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + optimistic: True + min_value: 0 + max_value: 720 #12 hours max + step: 5 mode: box - optimistic: true - restore_value: true - initial_value: 100 - + initial_value: 5 - platform: template - name: "${scheduler_unique_id} Scheduler Begin Hour" - id: ${scheduler_unique_id}_scheduler_begin_hour + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: "${scheduler_unique_id}_scheduler_begin_min" + optimistic: True min_value: 0 - max_value: 23 - step: 1 + max_value: 55 + step: 5 mode: box - optimistic: true - restore_value: true initial_value: 0 - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Minute" - id: ${scheduler_unique_id}_scheduler_begin_min + name: "${scheduler_unique_id} Scheduler End Minute" + id: "${scheduler_unique_id}_scheduler_end_min" + optimistic: True min_value: 0 max_value: 55 step: 5 mode: box - optimistic: true - restore_value: true - + initial_value: 0 - platform: template - name: "${scheduler_unique_id} Scheduler End Hour" - id: ${scheduler_unique_id}_scheduler_end_hour + name: "${scheduler_unique_id} Scheduler Begin Hour" + id: "${scheduler_unique_id}_scheduler_begin_hour" + optimistic: True min_value: 0 max_value: 23 step: 1 mode: box - optimistic: true - restore_value: true - initial_value: 2 - + initial_value: 22 - platform: template - name: "${scheduler_unique_id} Scheduler End Minute" - id: ${scheduler_unique_id}_scheduler_end_min + name: "${scheduler_unique_id} Scheduler End Hour" + id: "${scheduler_unique_id}_scheduler_end_hour" + optimistic: True min_value: 0 - max_value: 55 - step: 5 + max_value: 23 + step: 1 mode: box - optimistic: true - restore_value: true + initial_value: 6 time: - platform: sntp id: ${scheduler_unique_id}_sntp on_time: + - seconds: 0 - minutes: /1 + minutes: /5 then: - - script.execute: ${scheduler_unique_id}_evaluate + - if: + condition: + - switch.is_on: ${scheduler_unique_id}_scheduler_activate + then: + - if: + condition: + lambda: |- + // We are between begin (included) and end (excluded) hour and minutes + int beginTotalMinutes = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + id(${scheduler_unique_id}_scheduler_begin_min).state; + int endTotalMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; + int checkTotalMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 + id(${scheduler_unique_id}_sntp).now().minute; -esphome: - on_boot: - priority: -100 - then: - - delay: 10s - - script.execute: ${scheduler_unique_id}_evaluate + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && checkTotalMinutes < endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || checkTotalMinutes < endTotalMinutes; + } + then: + # Disable Router then set router level to X percent + - switch.turn_off: activate + - number.set: + id: router_level + value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; + # if deactivation script option + - script.execute: ${custom_script} + else: + - if: + condition: + and: + - switch.is_off: activate + # End Hour and minutes are reached + - lambda: |- + // End Hour and minutes are reached + if (id(${scheduler_unique_id}_scheduler_end_hour).state == id(${scheduler_unique_id}_sntp).now().hour && id(${scheduler_unique_id}_scheduler_end_min).state == id(${scheduler_unique_id}_sntp).now().minute){ + return true; + } + + // Or we are between end hour and minutes and scheduler_checking_end_threshold + // It's a failsafe to check that router have been activated even if we lost the moment of end hour and minutes + int beginTotalMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; + int endTotalMinutes = beginTotalMinutes + id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; + int checkTotalMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 + id(${scheduler_unique_id}_sntp).now().minute; + + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && checkTotalMinutes <= endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || checkTotalMinutes <= endTotalMinutes; + } + then: + # Set router level to 0 then Enable router + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate From 876a8ee87c5b32abe4db7727dbd8719776b1dddf Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 18:45:05 +0100 Subject: [PATCH 12/39] Create scheduler_forced_run_safe_beta.yaml --- .../scheduler_forced_run_safe_beta.yaml | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 solar_router/scheduler_forced_run_safe_beta.yaml diff --git a/solar_router/scheduler_forced_run_safe_beta.yaml b/solar_router/scheduler_forced_run_safe_beta.yaml new file mode 100644 index 00000000..8e0e34b7 --- /dev/null +++ b/solar_router/scheduler_forced_run_safe_beta.yaml @@ -0,0 +1,188 @@ +# Differences vs original scheduler: +# 1) Stops the scheduler automatically if safety temperature is reached +# 2) Uses existing stop_temperature value (no hardcoded threshold) +# 3) Temperature is checked periodically during scheduler time window +# 4) Adds logs to debug temperature readings and decisions + +substitutions: + scheduler_unique_id: "Forced" + custom_script: ${scheduler_unique_id}_fake_script + +script: + # Fake empty script if user does not provide one + - id: ${scheduler_unique_id}_fake_script + then: + + # Check temperature and stop scheduler if limit is reached + - id: check_temperature_for_${scheduler_unique_id}_scheduler + mode: single + then: + - lambda: |- + ESP_LOGI( + "scheduler_temp", + "Temp check: %.2f °C (stop at %.2f °C)", + id(safety_temperature).state, + id(stop_temperature).state + ); + + if (!isnan(id(safety_temperature).state) && + id(safety_temperature).state >= id(stop_temperature).state) { + ESP_LOGW( + "scheduler_temp", + "Temperature limit reached → stopping scheduler" + ); + id(${scheduler_unique_id}_scheduler_activate).turn_off(); + } + +switch: + # Scheduler enable/disable + - platform: template + name: "Activate ${scheduler_unique_id} Scheduler" + id: "${scheduler_unique_id}_scheduler_activate" + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + on_turn_off: + then: + - if: + condition: + - switch.is_off: activate + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate + +number: + - platform: template + name: "${scheduler_unique_id} Scheduler Router Level" + id: "${scheduler_unique_id}_scheduler_router_level" + min_value: 0 + max_value: 100 + step: 1 + unit_of_measurement: "%" + optimistic: true + mode: slider + restore_value: true + initial_value: 100 + + - platform: template + name: "${scheduler_unique_id} Scheduler Checking End Threshold" + id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + min_value: 0 + max_value: 720 + step: 5 + mode: box + optimistic: true + restore_value: true + initial_value: 5 + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: "${scheduler_unique_id}_scheduler_begin_min" + min_value: 0 + max_value: 55 + step: 5 + mode: box + optimistic: true + restore_value: true + initial_value: 0 + + - platform: template + name: "${scheduler_unique_id} Scheduler End Minute" + id: "${scheduler_unique_id}_scheduler_end_min" + min_value: 0 + max_value: 55 + step: 5 + mode: box + optimistic: true + restore_value: true + initial_value: 0 + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Hour" + id: "${scheduler_unique_id}_scheduler_begin_hour" + min_value: 0 + max_value: 23 + step: 1 + mode: box + optimistic: true + restore_value: true + initial_value: 22 + + - platform: template + name: "${scheduler_unique_id} Scheduler End Hour" + id: "${scheduler_unique_id}_scheduler_end_hour" + min_value: 0 + max_value: 23 + step: 1 + mode: box + optimistic: true + restore_value: true + initial_value: 6 + +time: + - platform: sntp + id: ${scheduler_unique_id}_sntp + on_time: + - seconds: 0 + minutes: /5 + then: + - if: + condition: + - switch.is_on: ${scheduler_unique_id}_scheduler_activate + then: + - if: + condition: + lambda: |- + int beginTotal = + id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + + int endTotal = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + + int nowTotal = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + + if (beginTotal <= endTotal) { + return nowTotal >= beginTotal && nowTotal < endTotal; + } else { + return nowTotal >= beginTotal || nowTotal < endTotal; + } + then: + # Scheduler active → force router level + - switch.turn_off: activate + - number.set: + id: router_level + value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; + + # Temperature safety check + - script.execute: check_temperature_for_${scheduler_unique_id}_scheduler + + # Optional custom script + - script.execute: ${custom_script} + + else: + - if: + condition: + and: + - switch.is_off: activate + - lambda: |- + int endTotal = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + + int nowTotal = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + + return nowTotal >= endTotal && + nowTotal <= endTotal + + id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate From 02ad6790d0dd5f571be594f51ac33346d3e2d85b Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 19:04:41 +0100 Subject: [PATCH 13/39] Delete solar_router/scheduler_forced_run_safe_beta.yaml --- .../scheduler_forced_run_safe_beta.yaml | 188 ------------------ 1 file changed, 188 deletions(-) delete mode 100644 solar_router/scheduler_forced_run_safe_beta.yaml diff --git a/solar_router/scheduler_forced_run_safe_beta.yaml b/solar_router/scheduler_forced_run_safe_beta.yaml deleted file mode 100644 index 8e0e34b7..00000000 --- a/solar_router/scheduler_forced_run_safe_beta.yaml +++ /dev/null @@ -1,188 +0,0 @@ -# Differences vs original scheduler: -# 1) Stops the scheduler automatically if safety temperature is reached -# 2) Uses existing stop_temperature value (no hardcoded threshold) -# 3) Temperature is checked periodically during scheduler time window -# 4) Adds logs to debug temperature readings and decisions - -substitutions: - scheduler_unique_id: "Forced" - custom_script: ${scheduler_unique_id}_fake_script - -script: - # Fake empty script if user does not provide one - - id: ${scheduler_unique_id}_fake_script - then: - - # Check temperature and stop scheduler if limit is reached - - id: check_temperature_for_${scheduler_unique_id}_scheduler - mode: single - then: - - lambda: |- - ESP_LOGI( - "scheduler_temp", - "Temp check: %.2f °C (stop at %.2f °C)", - id(safety_temperature).state, - id(stop_temperature).state - ); - - if (!isnan(id(safety_temperature).state) && - id(safety_temperature).state >= id(stop_temperature).state) { - ESP_LOGW( - "scheduler_temp", - "Temperature limit reached → stopping scheduler" - ); - id(${scheduler_unique_id}_scheduler_activate).turn_off(); - } - -switch: - # Scheduler enable/disable - - platform: template - name: "Activate ${scheduler_unique_id} Scheduler" - id: "${scheduler_unique_id}_scheduler_activate" - optimistic: true - restore_mode: RESTORE_DEFAULT_ON - on_turn_off: - then: - - if: - condition: - - switch.is_off: activate - then: - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate - -number: - - platform: template - name: "${scheduler_unique_id} Scheduler Router Level" - id: "${scheduler_unique_id}_scheduler_router_level" - min_value: 0 - max_value: 100 - step: 1 - unit_of_measurement: "%" - optimistic: true - mode: slider - restore_value: true - initial_value: 100 - - - platform: template - name: "${scheduler_unique_id} Scheduler Checking End Threshold" - id: "${scheduler_unique_id}_scheduler_checking_end_threshold" - min_value: 0 - max_value: 720 - step: 5 - mode: box - optimistic: true - restore_value: true - initial_value: 5 - - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Minute" - id: "${scheduler_unique_id}_scheduler_begin_min" - min_value: 0 - max_value: 55 - step: 5 - mode: box - optimistic: true - restore_value: true - initial_value: 0 - - - platform: template - name: "${scheduler_unique_id} Scheduler End Minute" - id: "${scheduler_unique_id}_scheduler_end_min" - min_value: 0 - max_value: 55 - step: 5 - mode: box - optimistic: true - restore_value: true - initial_value: 0 - - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Hour" - id: "${scheduler_unique_id}_scheduler_begin_hour" - min_value: 0 - max_value: 23 - step: 1 - mode: box - optimistic: true - restore_value: true - initial_value: 22 - - - platform: template - name: "${scheduler_unique_id} Scheduler End Hour" - id: "${scheduler_unique_id}_scheduler_end_hour" - min_value: 0 - max_value: 23 - step: 1 - mode: box - optimistic: true - restore_value: true - initial_value: 6 - -time: - - platform: sntp - id: ${scheduler_unique_id}_sntp - on_time: - - seconds: 0 - minutes: /5 - then: - - if: - condition: - - switch.is_on: ${scheduler_unique_id}_scheduler_activate - then: - - if: - condition: - lambda: |- - int beginTotal = - id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_begin_min).state; - - int endTotal = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - - int nowTotal = - id(${scheduler_unique_id}_sntp).now().hour * 60 + - id(${scheduler_unique_id}_sntp).now().minute; - - if (beginTotal <= endTotal) { - return nowTotal >= beginTotal && nowTotal < endTotal; - } else { - return nowTotal >= beginTotal || nowTotal < endTotal; - } - then: - # Scheduler active → force router level - - switch.turn_off: activate - - number.set: - id: router_level - value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; - - # Temperature safety check - - script.execute: check_temperature_for_${scheduler_unique_id}_scheduler - - # Optional custom script - - script.execute: ${custom_script} - - else: - - if: - condition: - and: - - switch.is_off: activate - - lambda: |- - int endTotal = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - - int nowTotal = - id(${scheduler_unique_id}_sntp).now().hour * 60 + - id(${scheduler_unique_id}_sntp).now().minute; - - return nowTotal >= endTotal && - nowTotal <= endTotal + - id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; - then: - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate From 5832a8539740a61cc19d682d565273f2f05f7267 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 19:16:59 +0100 Subject: [PATCH 14/39] Create scheduler_forced_run_safe_beta.yaml --- .../scheduler_forced_run_safe_beta.yaml | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 solar_router/scheduler_forced_run_safe_beta.yaml diff --git a/solar_router/scheduler_forced_run_safe_beta.yaml b/solar_router/scheduler_forced_run_safe_beta.yaml new file mode 100644 index 00000000..a9189403 --- /dev/null +++ b/solar_router/scheduler_forced_run_safe_beta.yaml @@ -0,0 +1,80 @@ +# Differences vs original scheduler: +# - Scheduler is automatically disabled when safety_limit (temperature reached) is true +# - Day/Night logic: day (08-19) blocks forced scheduler if water is hot enough +# - Forced scheduler only runs at night if temperature was NOT reached during the day +# - Fully autonomous: no Home Assistant automation needed + +substitutions: + scheduler_unique_id: "Forced" + custom_script: ${scheduler_unique_id}_fake_script + +script: + - id: ${scheduler_unique_id}_fake_script + then: + + # Main autonomy logic + - id: ${scheduler_unique_id}_auto_manager + mode: restart + then: + - lambda: |- + auto now = id(${scheduler_unique_id}_sntp).now(); + if (!now.is_valid()) return; + + int minutes = now.hour * 60 + now.minute; + bool is_day = (minutes >= 8 * 60) && (minutes < 19 * 60); + + ESP_LOGI("scheduler", "Temp=%.1f / Limit=%d / Day=%d", + id(safety_temperature).state, + id(safety_limit), + is_day); + + // Temperature reached → everything stops + if (id(safety_limit)) { + id(${scheduler_unique_id}_scheduler_activate).turn_off(); + id(activate).turn_on(); // re-enable router control + id(router_level).set(0); + return; + } + + // Daytime: do NOT allow forced scheduler + if (is_day) { + id(${scheduler_unique_id}_scheduler_activate).turn_off(); + return; + } + + // Night AND temperature not reached → allow forced scheduler + id(${scheduler_unique_id}_scheduler_activate).turn_on(); + +switch: + - platform: template + name: "Activate ${scheduler_unique_id} Scheduler" + optimistic: true + restore_mode: RESTORE_DEFAULT_OFF + id: "${scheduler_unique_id}_scheduler_activate" + +number: + - platform: template + name: "${scheduler_unique_id} Scheduler Router Level" + id: "${scheduler_unique_id}_scheduler_router_level" + min_value: 0 + max_value: 100 + initial_value: 100 + step: 1 + optimistic: true + mode: slider + +time: + - platform: sntp + id: ${scheduler_unique_id}_sntp + on_time: + # Every 5 minutes: autonomy manager + - seconds: 0 + minutes: /5 + then: + - script.execute: ${scheduler_unique_id}_auto_manager + +esphome: + on_boot: + priority: -1000 + then: + - script.execute: ${scheduler_unique_id}_auto_manager From d82c79b0bfcc6d0292cc9249c7a7ea0794c6679c Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 19:55:57 +0100 Subject: [PATCH 15/39] Update scheduler_forced_run_safe_beta.yaml --- .../scheduler_forced_run_safe_beta.yaml | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/solar_router/scheduler_forced_run_safe_beta.yaml b/solar_router/scheduler_forced_run_safe_beta.yaml index a9189403..92db5151 100644 --- a/solar_router/scheduler_forced_run_safe_beta.yaml +++ b/solar_router/scheduler_forced_run_safe_beta.yaml @@ -1,18 +1,26 @@ # Differences vs original scheduler: -# - Scheduler is automatically disabled when safety_limit (temperature reached) is true -# - Day/Night logic: day (08-19) blocks forced scheduler if water is hot enough -# - Forced scheduler only runs at night if temperature was NOT reached during the day -# - Fully autonomous: no Home Assistant automation needed +# - Forced scheduler is blocked during the day if temperature is reached +# - Night forced run only happens if water was NOT hot enough during the day +# - Safety_limit immediately stops forced run +# - No C++ calls, ESPHome-safe logic only substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script +globals: + # Memorize if temperature was reached during the day + - id: day_temperature_reached + type: bool + restore_value: true + initial_value: "false" + script: + # Dummy script - id: ${scheduler_unique_id}_fake_script then: - # Main autonomy logic + # Central autonomy logic - id: ${scheduler_unique_id}_auto_manager mode: restart then: @@ -23,27 +31,33 @@ script: int minutes = now.hour * 60 + now.minute; bool is_day = (minutes >= 8 * 60) && (minutes < 19 * 60); - ESP_LOGI("scheduler", "Temp=%.1f / Limit=%d / Day=%d", - id(safety_temperature).state, - id(safety_limit), - is_day); + ESP_LOGI("scheduler", + "Temp=%.1f Safety=%d Day=%d DayReached=%d", + id(safety_temperature).state, + id(safety_limit), + is_day, + id(day_temperature_reached) + ); - // Temperature reached → everything stops + // Temperature reached at any moment if (id(safety_limit)) { id(${scheduler_unique_id}_scheduler_activate).turn_off(); - id(activate).turn_on(); // re-enable router control - id(router_level).set(0); + id(day_temperature_reached) = is_day ? true : id(day_temperature_reached); return; } - // Daytime: do NOT allow forced scheduler + // Daytime: never allow forced scheduler if (is_day) { id(${scheduler_unique_id}_scheduler_activate).turn_off(); return; } - // Night AND temperature not reached → allow forced scheduler - id(${scheduler_unique_id}_scheduler_activate).turn_on(); + // Night: allow forced scheduler ONLY if water was NOT hot during the day + if (!id(day_temperature_reached)) { + id(${scheduler_unique_id}_scheduler_activate).turn_on(); + } else { + id(${scheduler_unique_id}_scheduler_activate).turn_off(); + } switch: - platform: template @@ -51,6 +65,12 @@ switch: optimistic: true restore_mode: RESTORE_DEFAULT_OFF id: "${scheduler_unique_id}_scheduler_activate" + on_turn_off: + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate number: - platform: template @@ -67,7 +87,6 @@ time: - platform: sntp id: ${scheduler_unique_id}_sntp on_time: - # Every 5 minutes: autonomy manager - seconds: 0 minutes: /5 then: From e60b39a4e177fcf3135bf09f4183178162e831a2 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:27:59 +0100 Subject: [PATCH 16/39] Update scheduler_forced_run_safe_beta.yaml --- .../scheduler_forced_run_safe_beta.yaml | 132 +++++++++++------- 1 file changed, 82 insertions(+), 50 deletions(-) diff --git a/solar_router/scheduler_forced_run_safe_beta.yaml b/solar_router/scheduler_forced_run_safe_beta.yaml index 92db5151..c181ab44 100644 --- a/solar_router/scheduler_forced_run_safe_beta.yaml +++ b/solar_router/scheduler_forced_run_safe_beta.yaml @@ -1,28 +1,20 @@ # Differences vs original scheduler: -# - Forced scheduler is blocked during the day if temperature is reached -# - Night forced run only happens if water was NOT hot enough during the day -# - Safety_limit immediately stops forced run -# - No C++ calls, ESPHome-safe logic only +# - Scheduler keeps all original time inputs visible in Home Assistant +# - Temperature safety can force stop the scheduler at any time +# - Day (08–19): temperature reached disables forced scheduler for the day +# - Night: scheduler runs only if temperature was not reached substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script -globals: - # Memorize if temperature was reached during the day - - id: day_temperature_reached - type: bool - restore_value: true - initial_value: "false" - script: - # Dummy script - id: ${scheduler_unique_id}_fake_script then: - # Central autonomy logic - - id: ${scheduler_unique_id}_auto_manager - mode: restart + # Temperature safety manager + - id: ${scheduler_unique_id}_temperature_guard + mode: single then: - lambda: |- auto now = id(${scheduler_unique_id}_sntp).now(); @@ -32,45 +24,41 @@ script: bool is_day = (minutes >= 8 * 60) && (minutes < 19 * 60); ESP_LOGI("scheduler", - "Temp=%.1f Safety=%d Day=%d DayReached=%d", + "Temp=%.1f / Safety=%d / Day=%d", id(safety_temperature).state, id(safety_limit), - is_day, - id(day_temperature_reached) + is_day ); - // Temperature reached at any moment - if (id(safety_limit)) { - id(${scheduler_unique_id}_scheduler_activate).turn_off(); - id(day_temperature_reached) = is_day ? true : id(day_temperature_reached); - return; - } - - // Daytime: never allow forced scheduler - if (is_day) { - id(${scheduler_unique_id}_scheduler_activate).turn_off(); - return; - } - - // Night: allow forced scheduler ONLY if water was NOT hot during the day - if (!id(day_temperature_reached)) { - id(${scheduler_unique_id}_scheduler_activate).turn_on(); - } else { - id(${scheduler_unique_id}_scheduler_activate).turn_off(); - } + - if: + condition: + lambda: |- + auto now = id(${scheduler_unique_id}_sntp).now(); + int minutes = now.hour * 60 + now.minute; + return id(safety_limit) && (minutes >= 8 * 60) && (minutes < 19 * 60); + then: + - switch.turn_off: ${scheduler_unique_id}_scheduler_activate + - switch.turn_on: activate + - number.set: + id: router_level + value: 0 switch: - platform: template name: "Activate ${scheduler_unique_id} Scheduler" optimistic: true - restore_mode: RESTORE_DEFAULT_OFF + restore_mode: RESTORE_DEFAULT_ON id: "${scheduler_unique_id}_scheduler_activate" on_turn_off: then: - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate + - if: + condition: + - switch.is_off: activate + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate number: - platform: template @@ -78,10 +66,60 @@ number: id: "${scheduler_unique_id}_scheduler_router_level" min_value: 0 max_value: 100 - initial_value: 100 step: 1 optimistic: true mode: slider + unit_of_measurement: "%" + + - platform: template + name: "${scheduler_unique_id} Scheduler Checking End Threshold" + id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + min_value: 0 + max_value: 720 + step: 5 + optimistic: true + mode: box + restore_value: true + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Hour" + id: "${scheduler_unique_id}_scheduler_begin_hour" + min_value: 0 + max_value: 23 + step: 1 + optimistic: true + mode: box + restore_value: true + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: "${scheduler_unique_id}_scheduler_begin_min" + min_value: 0 + max_value: 55 + step: 5 + optimistic: true + mode: box + restore_value: true + + - platform: template + name: "${scheduler_unique_id} Scheduler End Hour" + id: "${scheduler_unique_id}_scheduler_end_hour" + min_value: 0 + max_value: 23 + step: 1 + optimistic: true + mode: box + restore_value: true + + - platform: template + name: "${scheduler_unique_id} Scheduler End Minute" + id: "${scheduler_unique_id}_scheduler_end_min" + min_value: 0 + max_value: 55 + step: 5 + optimistic: true + mode: box + restore_value: true time: - platform: sntp @@ -90,10 +128,4 @@ time: - seconds: 0 minutes: /5 then: - - script.execute: ${scheduler_unique_id}_auto_manager - -esphome: - on_boot: - priority: -1000 - then: - - script.execute: ${scheduler_unique_id}_auto_manager + - script.execute: ${scheduler_unique_id}_temperature_guard From 930962fdd206ff4389dfc80dca9997e2ead7b88f Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:50:49 +0100 Subject: [PATCH 17/39] Update scheduler_forced_run_safe_beta.yaml --- .../scheduler_forced_run_safe_beta.yaml | 152 +++++++++++------- 1 file changed, 91 insertions(+), 61 deletions(-) diff --git a/solar_router/scheduler_forced_run_safe_beta.yaml b/solar_router/scheduler_forced_run_safe_beta.yaml index c181ab44..7c5c1ffe 100644 --- a/solar_router/scheduler_forced_run_safe_beta.yaml +++ b/solar_router/scheduler_forced_run_safe_beta.yaml @@ -1,49 +1,20 @@ -# Differences vs original scheduler: -# - Scheduler keeps all original time inputs visible in Home Assistant -# - Temperature safety can force stop the scheduler at any time -# - Day (08–19): temperature reached disables forced scheduler for the day -# - Night: scheduler runs only if temperature was not reached - +# Forced scheduler with temperature stop condition +# - The scheduler runs normally during the configured time range +# - If the water temperature reaches the configured limit while the scheduler is active, +# the forced scheduler switch is turned OFF immediately +# - No other behavior is modified: no restart logic, no day/night logic added +# - If the temperature is not reached, the scheduler runs until the normal end time substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script script: + # A fake empty script to run if user don't provide a custom one - id: ${scheduler_unique_id}_fake_script then: - # Temperature safety manager - - id: ${scheduler_unique_id}_temperature_guard - mode: single - then: - - lambda: |- - auto now = id(${scheduler_unique_id}_sntp).now(); - if (!now.is_valid()) return; - - int minutes = now.hour * 60 + now.minute; - bool is_day = (minutes >= 8 * 60) && (minutes < 19 * 60); - - ESP_LOGI("scheduler", - "Temp=%.1f / Safety=%d / Day=%d", - id(safety_temperature).state, - id(safety_limit), - is_day - ); - - - if: - condition: - lambda: |- - auto now = id(${scheduler_unique_id}_sntp).now(); - int minutes = now.hour * 60 + now.minute; - return id(safety_limit) && (minutes >= 8 * 60) && (minutes < 19 * 60); - then: - - switch.turn_off: ${scheduler_unique_id}_scheduler_activate - - switch.turn_on: activate - - number.set: - id: router_level - value: 0 - switch: + # Define if scheduler is active or not - platform: template name: "Activate ${scheduler_unique_id} Scheduler" optimistic: true @@ -55,6 +26,7 @@ switch: condition: - switch.is_off: activate then: + # Stop heating cleanly - number.set: id: router_level value: 0 @@ -66,60 +38,61 @@ number: id: "${scheduler_unique_id}_scheduler_router_level" min_value: 0 max_value: 100 + initial_value: 100 step: 1 + unit_of_measurement: "%" optimistic: true mode: slider - unit_of_measurement: "%" - platform: template name: "${scheduler_unique_id} Scheduler Checking End Threshold" id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + optimistic: true min_value: 0 max_value: 720 step: 5 - optimistic: true mode: box - restore_value: true + initial_value: 5 - platform: template - name: "${scheduler_unique_id} Scheduler Begin Hour" - id: "${scheduler_unique_id}_scheduler_begin_hour" - min_value: 0 - max_value: 23 - step: 1 + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: "${scheduler_unique_id}_scheduler_begin_min" optimistic: true + min_value: 0 + max_value: 55 + step: 5 mode: box - restore_value: true + initial_value: 0 - platform: template - name: "${scheduler_unique_id} Scheduler Begin Minute" - id: "${scheduler_unique_id}_scheduler_begin_min" + name: "${scheduler_unique_id} Scheduler End Minute" + id: "${scheduler_unique_id}_scheduler_end_min" + optimistic: true min_value: 0 max_value: 55 step: 5 - optimistic: true mode: box - restore_value: true + initial_value: 0 - platform: template - name: "${scheduler_unique_id} Scheduler End Hour" - id: "${scheduler_unique_id}_scheduler_end_hour" + name: "${scheduler_unique_id} Scheduler Begin Hour" + id: "${scheduler_unique_id}_scheduler_begin_hour" + optimistic: true min_value: 0 max_value: 23 step: 1 - optimistic: true mode: box - restore_value: true + initial_value: 22 - platform: template - name: "${scheduler_unique_id} Scheduler End Minute" - id: "${scheduler_unique_id}_scheduler_end_min" - min_value: 0 - max_value: 55 - step: 5 + name: "${scheduler_unique_id} Scheduler End Hour" + id: "${scheduler_unique_id}_scheduler_end_hour" optimistic: true + min_value: 0 + max_value: 23 + step: 1 mode: box - restore_value: true + initial_value: 6 time: - platform: sntp @@ -128,4 +101,61 @@ time: - seconds: 0 minutes: /5 then: - - script.execute: ${scheduler_unique_id}_temperature_guard + - if: + condition: + - switch.is_on: ${scheduler_unique_id}_scheduler_activate + then: + - if: + condition: + lambda: |- + int beginTotalMinutes = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + int endTotalMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int nowMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + + bool in_range; + if (beginTotalMinutes <= endTotalMinutes) { + in_range = nowMinutes >= beginTotalMinutes && nowMinutes < endTotalMinutes; + } else { + in_range = nowMinutes >= beginTotalMinutes || nowMinutes < endTotalMinutes; + } + + return in_range; + then: + # 🔥 SAFETY TEMPERATURE CHECK 🔥 + - if: + condition: + lambda: |- + return id(safety_limit); + then: + - logger.log: + level: WARN + format: "Forced scheduler stopped: temperature limit reached (%.1f°C)" + args: [ 'id(safety_temperature).state' ] + - switch.turn_off: ${scheduler_unique_id}_scheduler_activate + else: + # Normal scheduler behavior + - switch.turn_off: activate + - number.set: + id: router_level + value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; + - script.execute: ${custom_script} + else: + - if: + condition: + and: + - switch.is_off: activate + - lambda: |- + int endMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int nowMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + int threshold = id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; + return nowMinutes >= endMinutes && nowMinutes <= (endMinutes + threshold); + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate From 1fb6aba1e965d84f74aa0efd8bc9e98a9463d651 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:14:18 +0100 Subject: [PATCH 18/39] Update scheduler_forced_run_safe_beta.yaml --- .../scheduler_forced_run_safe_beta.yaml | 115 +++++++++++------- 1 file changed, 73 insertions(+), 42 deletions(-) diff --git a/solar_router/scheduler_forced_run_safe_beta.yaml b/solar_router/scheduler_forced_run_safe_beta.yaml index 7c5c1ffe..3907b3cb 100644 --- a/solar_router/scheduler_forced_run_safe_beta.yaml +++ b/solar_router/scheduler_forced_run_safe_beta.yaml @@ -1,9 +1,3 @@ -# Forced scheduler with temperature stop condition -# - The scheduler runs normally during the configured time range -# - If the water temperature reaches the configured limit while the scheduler is active, -# the forced scheduler switch is turned OFF immediately -# - No other behavior is modified: no restart logic, no day/night logic added -# - If the temperature is not reached, the scheduler runs until the normal end time substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script @@ -13,6 +7,25 @@ script: - id: ${scheduler_unique_id}_fake_script then: + # === SAFETY CHECK SCRIPT === + # Disable forced scheduler if temperature or safety limit is reached + - id: ${scheduler_unique_id}_temperature_guard + mode: single + then: + - lambda: |- + if ( + id(safety_limit) || + (!isnan(id(safety_temperature).state) && + id(safety_temperature).state >= id(stop_temperature).state) + ) { + ESP_LOGW("forced_scheduler", + "STOP scheduler: temp=%.1f limit=%d", + id(safety_temperature).state, + id(safety_limit) + ); + id(${scheduler_unique_id}_scheduler_activate).turn_off(); + } + switch: # Define if scheduler is active or not - platform: template @@ -26,13 +39,14 @@ switch: condition: - switch.is_off: activate then: - # Stop heating cleanly + # Set router level to 0 then Enable router - number.set: id: router_level value: 0 - switch.turn_on: activate number: + # Scheduler Router level from 0 to 100 - platform: template name: "${scheduler_unique_id} Scheduler Router Level" id: "${scheduler_unique_id}_scheduler_router_level" @@ -98,6 +112,14 @@ time: - platform: sntp id: ${scheduler_unique_id}_sntp on_time: + + # === Every 1 minute: temperature safety check === + - seconds: 0 + minutes: /1 + then: + - script.execute: ${scheduler_unique_id}_temperature_guard + + # === Existing scheduler logic (unchanged) === - seconds: 0 minutes: /5 then: @@ -108,52 +130,61 @@ time: - if: condition: lambda: |- - int beginTotalMinutes = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 - + id(${scheduler_unique_id}_scheduler_begin_min).state; - int endTotalMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 - + id(${scheduler_unique_id}_scheduler_end_min).state; - int nowMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 - + id(${scheduler_unique_id}_sntp).now().minute; + int beginTotalMinutes = + id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + int endTotalMinutes = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int checkTotalMinutes = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; - bool in_range; if (beginTotalMinutes <= endTotalMinutes) { - in_range = nowMinutes >= beginTotalMinutes && nowMinutes < endTotalMinutes; + return checkTotalMinutes >= beginTotalMinutes && + checkTotalMinutes < endTotalMinutes; } else { - in_range = nowMinutes >= beginTotalMinutes || nowMinutes < endTotalMinutes; + return checkTotalMinutes >= beginTotalMinutes || + checkTotalMinutes < endTotalMinutes; } - - return in_range; then: - # 🔥 SAFETY TEMPERATURE CHECK 🔥 - - if: - condition: - lambda: |- - return id(safety_limit); - then: - - logger.log: - level: WARN - format: "Forced scheduler stopped: temperature limit reached (%.1f°C)" - args: [ 'id(safety_temperature).state' ] - - switch.turn_off: ${scheduler_unique_id}_scheduler_activate - else: - # Normal scheduler behavior - - switch.turn_off: activate - - number.set: - id: router_level - value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; - - script.execute: ${custom_script} + - switch.turn_off: activate + - number.set: + id: router_level + value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; + - script.execute: ${custom_script} else: - if: condition: and: - switch.is_off: activate - lambda: |- - int endMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 - + id(${scheduler_unique_id}_scheduler_end_min).state; - int nowMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 - + id(${scheduler_unique_id}_sntp).now().minute; - int threshold = id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; - return nowMinutes >= endMinutes && nowMinutes <= (endMinutes + threshold); + if ( + id(${scheduler_unique_id}_scheduler_end_hour).state == + id(${scheduler_unique_id}_sntp).now().hour && + id(${scheduler_unique_id}_scheduler_end_min).state == + id(${scheduler_unique_id}_sntp).now().minute + ) { + return true; + } + + int beginTotalMinutes = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int endTotalMinutes = + beginTotalMinutes + + id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; + int checkTotalMinutes = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && + checkTotalMinutes <= endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || + checkTotalMinutes <= endTotalMinutes; + } then: - number.set: id: router_level From e6db24d7cafc52c3aa5fcd281957c9c0dd77aece Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:43:52 +0100 Subject: [PATCH 19/39] Update scheduler_forced_run_safe_beta.yaml --- solar_router/scheduler_forced_run_safe_beta.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/solar_router/scheduler_forced_run_safe_beta.yaml b/solar_router/scheduler_forced_run_safe_beta.yaml index 3907b3cb..2c79ede9 100644 --- a/solar_router/scheduler_forced_run_safe_beta.yaml +++ b/solar_router/scheduler_forced_run_safe_beta.yaml @@ -1,3 +1,4 @@ +#stop scheduler when temp is above stop_temperature or Safety limit reached is activated substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script From 979eefbdc4cef91209947ddefec660aacb9f48c7 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 22:02:59 +0100 Subject: [PATCH 20/39] Create scheduler_forced_run_temperature_guard.yaml --- scheduler_forced_run_temperature_guard.yaml | 210 ++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 scheduler_forced_run_temperature_guard.yaml diff --git a/scheduler_forced_run_temperature_guard.yaml b/scheduler_forced_run_temperature_guard.yaml new file mode 100644 index 00000000..3d8831ac --- /dev/null +++ b/scheduler_forced_run_temperature_guard.yaml @@ -0,0 +1,210 @@ +# Stop Forced Scheduler when temperature is above stop_temperature +# or when Safety limit reached is active +# Check is done every minute, ONLY during scheduler time window + +substitutions: + scheduler_unique_id: "Forced" + custom_script: ${scheduler_unique_id}_fake_script + +script: + # A fake empty script to run if user don't provide a custom one + - id: ${scheduler_unique_id}_fake_script + then: + + # === TEMPERATURE / SAFETY GUARD === + - id: ${scheduler_unique_id}_temperature_guard + mode: single + then: + - lambda: |- + auto now = id(${scheduler_unique_id}_sntp).now(); + if (!now.is_valid()) return; + + int now_min = now.hour * 60 + now.minute; + int begin_min = + id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + int end_min = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + + bool in_scheduler_window; + if (begin_min <= end_min) { + in_scheduler_window = (now_min >= begin_min && now_min < end_min); + } else { + in_scheduler_window = (now_min >= begin_min || now_min < end_min); + } + + if (!in_scheduler_window) return; + + bool temp_reached = + !isnan(id(safety_temperature).state) && + id(safety_temperature).state >= id(stop_temperature).state; + + if (id(safety_limit) || temp_reached) { + ESP_LOGW("forced_scheduler", + "Stopping Forced Scheduler (temp=%.1f / safety=%d)", + id(safety_temperature).state, + id(safety_limit) + ); + id(${scheduler_unique_id}_scheduler_activate).turn_off(); + } + +switch: + # Define if scheduler is active or not + - platform: template + name: "Activate ${scheduler_unique_id} Scheduler" + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + id: "${scheduler_unique_id}_scheduler_activate" + on_turn_off: + then: + - if: + condition: + - switch.is_off: activate + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate + +number: + # Scheduler Router level from 0 to 100 + - platform: template + name: "${scheduler_unique_id} Scheduler Router Level" + id: "${scheduler_unique_id}_scheduler_router_level" + min_value: 0 + max_value: 100 + initial_value: 100 + step: 1 + unit_of_measurement: "%" + optimistic: true + mode: slider + + - platform: template + name: "${scheduler_unique_id} Scheduler Checking End Threshold" + id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + optimistic: true + min_value: 0 + max_value: 720 + step: 5 + mode: box + initial_value: 5 + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: "${scheduler_unique_id}_scheduler_begin_min" + optimistic: true + min_value: 0 + max_value: 55 + step: 5 + mode: box + initial_value: 0 + + - platform: template + name: "${scheduler_unique_id} Scheduler End Minute" + id: "${scheduler_unique_id}_scheduler_end_min" + optimistic: true + min_value: 0 + max_value: 55 + step: 5 + mode: box + initial_value: 0 + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Hour" + id: "${scheduler_unique_id}_scheduler_begin_hour" + optimistic: true + min_value: 0 + max_value: 23 + step: 1 + mode: box + initial_value: 22 + + - platform: template + name: "${scheduler_unique_id} Scheduler End Hour" + id: "${scheduler_unique_id}_scheduler_end_hour" + optimistic: true + min_value: 0 + max_value: 23 + step: 1 + mode: box + initial_value: 6 + +time: + - platform: sntp + id: ${scheduler_unique_id}_sntp + on_time: + # === Every 1 minute: safety temperature guard === + - seconds: 0 + minutes: /1 + then: + - script.execute: ${scheduler_unique_id}_temperature_guard + + # === Existing scheduler logic (UNCHANGED) === + - seconds: 0 + minutes: /5 + then: + - if: + condition: + - switch.is_on: ${scheduler_unique_id}_scheduler_activate + then: + - if: + condition: + lambda: |- + int beginTotalMinutes = + id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + int endTotalMinutes = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int checkTotalMinutes = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && + checkTotalMinutes < endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || + checkTotalMinutes < endTotalMinutes; + } + then: + - switch.turn_off: activate + - number.set: + id: router_level + value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; + - script.execute: ${custom_script} + else: + - if: + condition: + and: + - switch.is_off: activate + - lambda: |- + if ( + id(${scheduler_unique_id}_scheduler_end_hour).state == + id(${scheduler_unique_id}_sntp).now().hour && + id(${scheduler_unique_id}_scheduler_end_min).state == + id(${scheduler_unique_id}_sntp).now().minute + ) { + return true; + } + int beginTotalMinutes = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int endTotalMinutes = + beginTotalMinutes + + id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; + int checkTotalMinutes = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && + checkTotalMinutes <= endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || + checkTotalMinutes <= endTotalMinutes; + } + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate From 10135d03975e7a9594a6232efd5803a344334700 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 22:04:31 +0100 Subject: [PATCH 21/39] Create scheduler_forced_run_temperature_guard.yaml # Stop Forced Scheduler when temperature is above stop_temperature # or when Safety limit reached is active # Check is done every minute, ONLY during scheduler time window --- ...cheduler_forced_run_temperature_guard.yaml | 210 ++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 solar_router/scheduler_forced_run_temperature_guard.yaml diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml new file mode 100644 index 00000000..3d8831ac --- /dev/null +++ b/solar_router/scheduler_forced_run_temperature_guard.yaml @@ -0,0 +1,210 @@ +# Stop Forced Scheduler when temperature is above stop_temperature +# or when Safety limit reached is active +# Check is done every minute, ONLY during scheduler time window + +substitutions: + scheduler_unique_id: "Forced" + custom_script: ${scheduler_unique_id}_fake_script + +script: + # A fake empty script to run if user don't provide a custom one + - id: ${scheduler_unique_id}_fake_script + then: + + # === TEMPERATURE / SAFETY GUARD === + - id: ${scheduler_unique_id}_temperature_guard + mode: single + then: + - lambda: |- + auto now = id(${scheduler_unique_id}_sntp).now(); + if (!now.is_valid()) return; + + int now_min = now.hour * 60 + now.minute; + int begin_min = + id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + int end_min = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + + bool in_scheduler_window; + if (begin_min <= end_min) { + in_scheduler_window = (now_min >= begin_min && now_min < end_min); + } else { + in_scheduler_window = (now_min >= begin_min || now_min < end_min); + } + + if (!in_scheduler_window) return; + + bool temp_reached = + !isnan(id(safety_temperature).state) && + id(safety_temperature).state >= id(stop_temperature).state; + + if (id(safety_limit) || temp_reached) { + ESP_LOGW("forced_scheduler", + "Stopping Forced Scheduler (temp=%.1f / safety=%d)", + id(safety_temperature).state, + id(safety_limit) + ); + id(${scheduler_unique_id}_scheduler_activate).turn_off(); + } + +switch: + # Define if scheduler is active or not + - platform: template + name: "Activate ${scheduler_unique_id} Scheduler" + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + id: "${scheduler_unique_id}_scheduler_activate" + on_turn_off: + then: + - if: + condition: + - switch.is_off: activate + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate + +number: + # Scheduler Router level from 0 to 100 + - platform: template + name: "${scheduler_unique_id} Scheduler Router Level" + id: "${scheduler_unique_id}_scheduler_router_level" + min_value: 0 + max_value: 100 + initial_value: 100 + step: 1 + unit_of_measurement: "%" + optimistic: true + mode: slider + + - platform: template + name: "${scheduler_unique_id} Scheduler Checking End Threshold" + id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + optimistic: true + min_value: 0 + max_value: 720 + step: 5 + mode: box + initial_value: 5 + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: "${scheduler_unique_id}_scheduler_begin_min" + optimistic: true + min_value: 0 + max_value: 55 + step: 5 + mode: box + initial_value: 0 + + - platform: template + name: "${scheduler_unique_id} Scheduler End Minute" + id: "${scheduler_unique_id}_scheduler_end_min" + optimistic: true + min_value: 0 + max_value: 55 + step: 5 + mode: box + initial_value: 0 + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Hour" + id: "${scheduler_unique_id}_scheduler_begin_hour" + optimistic: true + min_value: 0 + max_value: 23 + step: 1 + mode: box + initial_value: 22 + + - platform: template + name: "${scheduler_unique_id} Scheduler End Hour" + id: "${scheduler_unique_id}_scheduler_end_hour" + optimistic: true + min_value: 0 + max_value: 23 + step: 1 + mode: box + initial_value: 6 + +time: + - platform: sntp + id: ${scheduler_unique_id}_sntp + on_time: + # === Every 1 minute: safety temperature guard === + - seconds: 0 + minutes: /1 + then: + - script.execute: ${scheduler_unique_id}_temperature_guard + + # === Existing scheduler logic (UNCHANGED) === + - seconds: 0 + minutes: /5 + then: + - if: + condition: + - switch.is_on: ${scheduler_unique_id}_scheduler_activate + then: + - if: + condition: + lambda: |- + int beginTotalMinutes = + id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + int endTotalMinutes = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int checkTotalMinutes = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && + checkTotalMinutes < endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || + checkTotalMinutes < endTotalMinutes; + } + then: + - switch.turn_off: activate + - number.set: + id: router_level + value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; + - script.execute: ${custom_script} + else: + - if: + condition: + and: + - switch.is_off: activate + - lambda: |- + if ( + id(${scheduler_unique_id}_scheduler_end_hour).state == + id(${scheduler_unique_id}_sntp).now().hour && + id(${scheduler_unique_id}_scheduler_end_min).state == + id(${scheduler_unique_id}_sntp).now().minute + ) { + return true; + } + int beginTotalMinutes = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int endTotalMinutes = + beginTotalMinutes + + id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; + int checkTotalMinutes = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && + checkTotalMinutes <= endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || + checkTotalMinutes <= endTotalMinutes; + } + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate From 85f07f829421618f2eea88c62026ebda4422b350 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 22:42:46 +0100 Subject: [PATCH 22/39] Update scheduler_forced_run_temperature_guard.yaml --- ...cheduler_forced_run_temperature_guard.yaml | 150 ++++++++---------- 1 file changed, 65 insertions(+), 85 deletions(-) diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml index 3d8831ac..fb4e0c35 100644 --- a/solar_router/scheduler_forced_run_temperature_guard.yaml +++ b/solar_router/scheduler_forced_run_temperature_guard.yaml @@ -1,16 +1,48 @@ -# Stop Forced Scheduler when temperature is above stop_temperature -# or when Safety limit reached is active -# Check is done every minute, ONLY during scheduler time window +# Forced Scheduler with Temperature Guard and Daily Temperature Memory +# - Stops scheduler when stop_temperature is reached or safety_limit is active +# - Remembers if temperature was reached during the day (08h–19h) +# - Prevents night forced heating if water was already hot +# - Fully autonomous and reboot-safe substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script +globals: + # Remember if temperature was reached during the day + - id: temperature_atteinte_journee + type: bool + restore_value: true + initial_value: "false" + script: - # A fake empty script to run if user don't provide a custom one + # Fake script (unchanged) - id: ${scheduler_unique_id}_fake_script then: + # === DAILY TEMPERATURE MEMORY UPDATE === + - id: ${scheduler_unique_id}_day_temperature_tracker + mode: single + then: + - lambda: |- + auto now = id(${scheduler_unique_id}_sntp).now(); + if (!now.is_valid()) return; + + int minutes = now.hour * 60 + now.minute; + bool is_day = (minutes >= 8 * 60) && (minutes < 19 * 60); + + if (!is_day) return; + + if ( + !isnan(id(safety_temperature).state) && + id(safety_temperature).state >= id(stop_temperature).state + ) { + if (!id(temperature_atteinte_journee)) { + ESP_LOGI("forced_scheduler", "Day temperature reached -> memorized"); + } + id(temperature_atteinte_journee) = true; + } + # === TEMPERATURE / SAFETY GUARD === - id: ${scheduler_unique_id}_temperature_guard mode: single @@ -50,7 +82,6 @@ script: } switch: - # Define if scheduler is active or not - platform: template name: "Activate ${scheduler_unique_id} Scheduler" optimistic: true @@ -67,8 +98,21 @@ switch: value: 0 - switch.turn_on: activate + # Visible in Home Assistant + - platform: template + name: "Temp atteinte aujourd'hui" + id: temperature_atteinte_journee_switch + optimistic: true + lambda: |- + return id(temperature_atteinte_journee); + turn_on_action: + - lambda: |- + id(temperature_atteinte_journee) = true; + turn_off_action: + - lambda: |- + id(temperature_atteinte_journee) = false; + number: - # Scheduler Router level from 0 to 100 - platform: template name: "${scheduler_unique_id} Scheduler Router Level" id: "${scheduler_unique_id}_scheduler_router_level" @@ -83,128 +127,64 @@ number: - platform: template name: "${scheduler_unique_id} Scheduler Checking End Threshold" id: "${scheduler_unique_id}_scheduler_checking_end_threshold" - optimistic: true min_value: 0 max_value: 720 step: 5 - mode: box initial_value: 5 + optimistic: true - platform: template name: "${scheduler_unique_id} Scheduler Begin Minute" id: "${scheduler_unique_id}_scheduler_begin_min" - optimistic: true min_value: 0 max_value: 55 step: 5 - mode: box initial_value: 0 + optimistic: true - platform: template name: "${scheduler_unique_id} Scheduler End Minute" id: "${scheduler_unique_id}_scheduler_end_min" - optimistic: true min_value: 0 max_value: 55 step: 5 - mode: box initial_value: 0 + optimistic: true - platform: template name: "${scheduler_unique_id} Scheduler Begin Hour" id: "${scheduler_unique_id}_scheduler_begin_hour" - optimistic: true min_value: 0 max_value: 23 step: 1 - mode: box initial_value: 22 + optimistic: true - platform: template name: "${scheduler_unique_id} Scheduler End Hour" id: "${scheduler_unique_id}_scheduler_end_hour" - optimistic: true min_value: 0 max_value: 23 step: 1 - mode: box initial_value: 6 + optimistic: true time: - platform: sntp id: ${scheduler_unique_id}_sntp on_time: - # === Every 1 minute: safety temperature guard === + # Reset daily memory at 08:00 - seconds: 0 - minutes: /1 + minutes: 0 + hours: 8 then: - - script.execute: ${scheduler_unique_id}_temperature_guard + - lambda: |- + ESP_LOGI("forced_scheduler", "Daily temperature memory reset"); + id(temperature_atteinte_journee) = false; - # === Existing scheduler logic (UNCHANGED) === + # Every minute: update memory + safety guard - seconds: 0 - minutes: /5 + minutes: /1 then: - - if: - condition: - - switch.is_on: ${scheduler_unique_id}_scheduler_activate - then: - - if: - condition: - lambda: |- - int beginTotalMinutes = - id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_begin_min).state; - int endTotalMinutes = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - int checkTotalMinutes = - id(${scheduler_unique_id}_sntp).now().hour * 60 + - id(${scheduler_unique_id}_sntp).now().minute; - if (beginTotalMinutes <= endTotalMinutes) { - return checkTotalMinutes >= beginTotalMinutes && - checkTotalMinutes < endTotalMinutes; - } else { - return checkTotalMinutes >= beginTotalMinutes || - checkTotalMinutes < endTotalMinutes; - } - then: - - switch.turn_off: activate - - number.set: - id: router_level - value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; - - script.execute: ${custom_script} - else: - - if: - condition: - and: - - switch.is_off: activate - - lambda: |- - if ( - id(${scheduler_unique_id}_scheduler_end_hour).state == - id(${scheduler_unique_id}_sntp).now().hour && - id(${scheduler_unique_id}_scheduler_end_min).state == - id(${scheduler_unique_id}_sntp).now().minute - ) { - return true; - } - int beginTotalMinutes = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - int endTotalMinutes = - beginTotalMinutes + - id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; - int checkTotalMinutes = - id(${scheduler_unique_id}_sntp).now().hour * 60 + - id(${scheduler_unique_id}_sntp).now().minute; - if (beginTotalMinutes <= endTotalMinutes) { - return checkTotalMinutes >= beginTotalMinutes && - checkTotalMinutes <= endTotalMinutes; - } else { - return checkTotalMinutes >= beginTotalMinutes || - checkTotalMinutes <= endTotalMinutes; - } - then: - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate + - script.execute: ${scheduler_unique_id}_day_temperature_tracker + - script.execute: ${scheduler_unique_id}_temperature_guard From 2cb9e308ff31174e0fdec1b656e70672da988142 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 22:55:09 +0100 Subject: [PATCH 23/39] Update scheduler_forced_run_temperature_guard.yaml --- ...cheduler_forced_run_temperature_guard.yaml | 109 ++++++++++-------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml index fb4e0c35..9f69b3bc 100644 --- a/solar_router/scheduler_forced_run_temperature_guard.yaml +++ b/solar_router/scheduler_forced_run_temperature_guard.yaml @@ -1,49 +1,32 @@ -# Forced Scheduler with Temperature Guard and Daily Temperature Memory -# - Stops scheduler when stop_temperature is reached or safety_limit is active -# - Remembers if temperature was reached during the day (08h–19h) -# - Prevents night forced heating if water was already hot -# - Fully autonomous and reboot-safe +# ============================================================ +# Forced Scheduler with Temperature Guard & Daily Memory +# +# - Stops Forced Scheduler if stop_temperature is reached +# - Stops Forced Scheduler if Safety limit is active +# - Remembers if temperature was reached during the day +# - Memory is persistent across reboot +# - Memory resets every day at 08:00 +# - Checks every minute +# ============================================================ substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script globals: - # Remember if temperature was reached during the day - - id: temperature_atteinte_journee + - id: temperature_atteinte_60 type: bool - restore_value: true + restore_value: yes initial_value: "false" script: - # Fake script (unchanged) + # Dummy script if user doesn't define one - id: ${scheduler_unique_id}_fake_script then: - # === DAILY TEMPERATURE MEMORY UPDATE === - - id: ${scheduler_unique_id}_day_temperature_tracker - mode: single - then: - - lambda: |- - auto now = id(${scheduler_unique_id}_sntp).now(); - if (!now.is_valid()) return; - - int minutes = now.hour * 60 + now.minute; - bool is_day = (minutes >= 8 * 60) && (minutes < 19 * 60); - - if (!is_day) return; - - if ( - !isnan(id(safety_temperature).state) && - id(safety_temperature).state >= id(stop_temperature).state - ) { - if (!id(temperature_atteinte_journee)) { - ESP_LOGI("forced_scheduler", "Day temperature reached -> memorized"); - } - id(temperature_atteinte_journee) = true; - } - - # === TEMPERATURE / SAFETY GUARD === + # ========================================================== + # TEMPERATURE / SAFETY GUARD + # ========================================================== - id: ${scheduler_unique_id}_temperature_guard mode: single then: @@ -52,9 +35,11 @@ script: if (!now.is_valid()) return; int now_min = now.hour * 60 + now.minute; + int begin_min = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + id(${scheduler_unique_id}_scheduler_begin_min).state; + int end_min = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; @@ -66,15 +51,36 @@ script: in_scheduler_window = (now_min >= begin_min || now_min < end_min); } - if (!in_scheduler_window) return; - bool temp_reached = !isnan(id(safety_temperature).state) && id(safety_temperature).state >= id(stop_temperature).state; - if (id(safety_limit) || temp_reached) { + // -------------------------------------------------- + // DAYTIME (08:00 → 19:00) : remember if temp reached + // -------------------------------------------------- + if (now.hour >= 8 && now.hour < 19) { + if (temp_reached || id(safety_limit)) { + id(temperature_atteinte_60) = true; + } + return; + } + + // -------------------------------------------------- + // NIGHT : block forced scheduler if already heated + // -------------------------------------------------- + if (id(temperature_atteinte_60)) { + id(${scheduler_unique_id}_scheduler_activate).turn_off(); + return; + } + + // -------------------------------------------------- + // Inside scheduler window → safety stop + // -------------------------------------------------- + if (!in_scheduler_window) return; + + if (temp_reached || id(safety_limit)) { ESP_LOGW("forced_scheduler", - "Stopping Forced Scheduler (temp=%.1f / safety=%d)", + "STOP Forced Scheduler (temp=%.1f safety=%d)", id(safety_temperature).state, id(safety_limit) ); @@ -98,19 +104,17 @@ switch: value: 0 - switch.turn_on: activate - # Visible in Home Assistant + # Exposed helper switch in Home Assistant - platform: template - name: "Temp atteinte aujourd'hui" - id: temperature_atteinte_journee_switch + name: "Temperature atteinte aujourd'hui" + id: temperature_atteinte_60_switch optimistic: true lambda: |- - return id(temperature_atteinte_journee); + return id(temperature_atteinte_60); turn_on_action: - - lambda: |- - id(temperature_atteinte_journee) = true; + - lambda: id(temperature_atteinte_60) = true; turn_off_action: - - lambda: |- - id(temperature_atteinte_journee) = false; + - lambda: id(temperature_atteinte_60) = false; number: - platform: template @@ -132,6 +136,7 @@ number: step: 5 initial_value: 5 optimistic: true + mode: box - platform: template name: "${scheduler_unique_id} Scheduler Begin Minute" @@ -141,6 +146,7 @@ number: step: 5 initial_value: 0 optimistic: true + mode: box - platform: template name: "${scheduler_unique_id} Scheduler End Minute" @@ -150,6 +156,7 @@ number: step: 5 initial_value: 0 optimistic: true + mode: box - platform: template name: "${scheduler_unique_id} Scheduler Begin Hour" @@ -159,6 +166,7 @@ number: step: 1 initial_value: 22 optimistic: true + mode: box - platform: template name: "${scheduler_unique_id} Scheduler End Hour" @@ -168,23 +176,22 @@ number: step: 1 initial_value: 6 optimistic: true + mode: box time: - platform: sntp id: ${scheduler_unique_id}_sntp on_time: # Reset daily memory at 08:00 - - seconds: 0 + - hours: 8 minutes: 0 - hours: 8 + seconds: 0 then: - lambda: |- - ESP_LOGI("forced_scheduler", "Daily temperature memory reset"); - id(temperature_atteinte_journee) = false; + id(temperature_atteinte_60) = false; - # Every minute: update memory + safety guard + # Temperature guard every minute - seconds: 0 minutes: /1 then: - - script.execute: ${scheduler_unique_id}_day_temperature_tracker - script.execute: ${scheduler_unique_id}_temperature_guard From f775a2e5d22455e7497644e30530bdf29c6e1016 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 22:59:25 +0100 Subject: [PATCH 24/39] Update scheduler_forced_run_temperature_guard.yaml --- ...cheduler_forced_run_temperature_guard.yaml | 130 ++++++++---------- 1 file changed, 57 insertions(+), 73 deletions(-) diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml index 9f69b3bc..3469a2a9 100644 --- a/solar_router/scheduler_forced_run_temperature_guard.yaml +++ b/solar_router/scheduler_forced_run_temperature_guard.yaml @@ -1,45 +1,53 @@ -# ============================================================ -# Forced Scheduler with Temperature Guard & Daily Memory -# -# - Stops Forced Scheduler if stop_temperature is reached -# - Stops Forced Scheduler if Safety limit is active -# - Remembers if temperature was reached during the day -# - Memory is persistent across reboot -# - Memory resets every day at 08:00 -# - Checks every minute -# ============================================================ +# Stop Forced Scheduler when temperature is above stop_temperature +# or when Safety limit reached is active +# Track if temperature was reached during the day (08h–19h) +# Variable is persistent and reset every day at 08:00 substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script -globals: - - id: temperature_atteinte_60 - type: bool - restore_value: yes - initial_value: "false" - script: - # Dummy script if user doesn't define one + # A fake empty script to run if user don't provide a custom one - id: ${scheduler_unique_id}_fake_script then: - # ========================================================== - # TEMPERATURE / SAFETY GUARD - # ========================================================== - - id: ${scheduler_unique_id}_temperature_guard + # === DAY TEMPERATURE TRACKING === + # If temperature reached during the day, mark it + - id: ${scheduler_unique_id}_day_temperature_tracker mode: single then: - lambda: |- auto now = id(${scheduler_unique_id}_sntp).now(); if (!now.is_valid()) return; + // Day window: 08:00 → 19:00 int now_min = now.hour * 60 + now.minute; + if (now_min < 8 * 60 || now_min >= 19 * 60) return; + + if ( + !isnan(id(safety_temperature).state) && + id(safety_temperature).state >= id(stop_temperature).state + ) { + if (!id(temperature_atteinte_60).state) { + ESP_LOGI("forced_scheduler", "Day temp reached → disabling Forced Scheduler tonight"); + id(temperature_atteinte_60).turn_on(); + } + } + + # === SAFETY CHECK SCRIPT === + # Disable forced scheduler if temperature or safety limit is reached + - id: ${scheduler_unique_id}_temperature_guard + mode: single + then: + - lambda: |- + auto now = id(${scheduler_unique_id}_sntp).now(); + if (!now.is_valid()) return; + int now_min = now.hour * 60 + now.minute; int begin_min = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + id(${scheduler_unique_id}_scheduler_begin_min).state; - int end_min = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; @@ -51,36 +59,15 @@ script: in_scheduler_window = (now_min >= begin_min || now_min < end_min); } + if (!in_scheduler_window) return; + bool temp_reached = !isnan(id(safety_temperature).state) && id(safety_temperature).state >= id(stop_temperature).state; - // -------------------------------------------------- - // DAYTIME (08:00 → 19:00) : remember if temp reached - // -------------------------------------------------- - if (now.hour >= 8 && now.hour < 19) { - if (temp_reached || id(safety_limit)) { - id(temperature_atteinte_60) = true; - } - return; - } - - // -------------------------------------------------- - // NIGHT : block forced scheduler if already heated - // -------------------------------------------------- - if (id(temperature_atteinte_60)) { - id(${scheduler_unique_id}_scheduler_activate).turn_off(); - return; - } - - // -------------------------------------------------- - // Inside scheduler window → safety stop - // -------------------------------------------------- - if (!in_scheduler_window) return; - - if (temp_reached || id(safety_limit)) { + if (id(safety_limit) || temp_reached) { ESP_LOGW("forced_scheduler", - "STOP Forced Scheduler (temp=%.1f safety=%d)", + "STOP Forced Scheduler (temp=%.1f / safety=%d)", id(safety_temperature).state, id(safety_limit) ); @@ -88,6 +75,14 @@ script: } switch: + # Track if temperature was reached during the day (persistent) + - platform: template + name: "Temp atteinte dans la journée" + id: temperature_atteinte_60 + optimistic: true + restore_mode: RESTORE_DEFAULT_OFF + + # Define if scheduler is active or not - platform: template name: "Activate ${scheduler_unique_id} Scheduler" optimistic: true @@ -104,19 +99,8 @@ switch: value: 0 - switch.turn_on: activate - # Exposed helper switch in Home Assistant - - platform: template - name: "Temperature atteinte aujourd'hui" - id: temperature_atteinte_60_switch - optimistic: true - lambda: |- - return id(temperature_atteinte_60); - turn_on_action: - - lambda: id(temperature_atteinte_60) = true; - turn_off_action: - - lambda: id(temperature_atteinte_60) = false; - number: + # Scheduler Router level from 0 to 100 - platform: template name: "${scheduler_unique_id} Scheduler Router Level" id: "${scheduler_unique_id}_scheduler_router_level" @@ -131,67 +115,67 @@ number: - platform: template name: "${scheduler_unique_id} Scheduler Checking End Threshold" id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + optimistic: true min_value: 0 max_value: 720 step: 5 - initial_value: 5 - optimistic: true mode: box + initial_value: 5 - platform: template name: "${scheduler_unique_id} Scheduler Begin Minute" id: "${scheduler_unique_id}_scheduler_begin_min" + optimistic: true min_value: 0 max_value: 55 step: 5 - initial_value: 0 - optimistic: true mode: box + initial_value: 0 - platform: template name: "${scheduler_unique_id} Scheduler End Minute" id: "${scheduler_unique_id}_scheduler_end_min" + optimistic: true min_value: 0 max_value: 55 step: 5 - initial_value: 0 - optimistic: true mode: box + initial_value: 0 - platform: template name: "${scheduler_unique_id} Scheduler Begin Hour" id: "${scheduler_unique_id}_scheduler_begin_hour" + optimistic: true min_value: 0 max_value: 23 step: 1 - initial_value: 22 - optimistic: true mode: box + initial_value: 22 - platform: template name: "${scheduler_unique_id} Scheduler End Hour" id: "${scheduler_unique_id}_scheduler_end_hour" + optimistic: true min_value: 0 max_value: 23 step: 1 - initial_value: 6 - optimistic: true mode: box + initial_value: 6 time: - platform: sntp id: ${scheduler_unique_id}_sntp on_time: - # Reset daily memory at 08:00 + # Reset daily flag at 08:00 - hours: 8 minutes: 0 seconds: 0 then: - - lambda: |- - id(temperature_atteinte_60) = false; + - switch.turn_off: temperature_atteinte_60 - # Temperature guard every minute + # Every minute: track day temperature + safety guard - seconds: 0 minutes: /1 then: + - script.execute: ${scheduler_unique_id}_day_temperature_tracker - script.execute: ${scheduler_unique_id}_temperature_guard From 2813bc6ce1c2977ed6db2d6f9474b1dc6e06efd2 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 23:22:15 +0100 Subject: [PATCH 25/39] Update scheduler_forced_run_temperature_guard.yaml --- ...cheduler_forced_run_temperature_guard.yaml | 110 +++++++++--------- 1 file changed, 58 insertions(+), 52 deletions(-) diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml index 3469a2a9..cb0491ee 100644 --- a/solar_router/scheduler_forced_run_temperature_guard.yaml +++ b/solar_router/scheduler_forced_run_temperature_guard.yaml @@ -2,18 +2,18 @@ # or when Safety limit reached is active # Track if temperature was reached during the day (08h–19h) # Variable is persistent and reset every day at 08:00 +# Forced Scheduler decision is evaluated at BOOT and at 19:00 substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script script: - # A fake empty script to run if user don't provide a custom one + # Fake empty script (compatibility) - id: ${scheduler_unique_id}_fake_script then: - # === DAY TEMPERATURE TRACKING === - # If temperature reached during the day, mark it + # === DAY TEMPERATURE TRACKING (08h–19h) === - id: ${scheduler_unique_id}_day_temperature_tracker mode: single then: @@ -21,7 +21,6 @@ script: auto now = id(${scheduler_unique_id}_sntp).now(); if (!now.is_valid()) return; - // Day window: 08:00 → 19:00 int now_min = now.hour * 60 + now.minute; if (now_min < 8 * 60 || now_min >= 19 * 60) return; @@ -30,13 +29,13 @@ script: id(safety_temperature).state >= id(stop_temperature).state ) { if (!id(temperature_atteinte_60).state) { - ESP_LOGI("forced_scheduler", "Day temp reached → disabling Forced Scheduler tonight"); + ESP_LOGI("forced_scheduler", + "Temperature reached during the day → disable forced tonight"); id(temperature_atteinte_60).turn_on(); } } - # === SAFETY CHECK SCRIPT === - # Disable forced scheduler if temperature or safety limit is reached + # === SAFETY GUARD DURING FORCED WINDOW === - id: ${scheduler_unique_id}_temperature_guard mode: single then: @@ -52,14 +51,13 @@ script: id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; - bool in_scheduler_window; - if (begin_min <= end_min) { - in_scheduler_window = (now_min >= begin_min && now_min < end_min); - } else { - in_scheduler_window = (now_min >= begin_min || now_min < end_min); - } + bool in_window; + if (begin_min <= end_min) + in_window = (now_min >= begin_min && now_min < end_min); + else + in_window = (now_min >= begin_min || now_min < end_min); - if (!in_scheduler_window) return; + if (!in_window) return; bool temp_reached = !isnan(id(safety_temperature).state) && @@ -67,27 +65,52 @@ script: if (id(safety_limit) || temp_reached) { ESP_LOGW("forced_scheduler", - "STOP Forced Scheduler (temp=%.1f / safety=%d)", + "STOP forced scheduler (temp=%.1f safety=%d)", id(safety_temperature).state, id(safety_limit) ); id(${scheduler_unique_id}_scheduler_activate).turn_off(); } + # === DECIDE IF FORCED SCHEDULER SHOULD RUN === + # Executed at BOOT and at 19:00 + - id: ${scheduler_unique_id}_forced_decision + mode: single + then: + - lambda: |- + auto now = id(${scheduler_unique_id}_sntp).now(); + if (!now.is_valid()) return; + + bool temp_ok = + !isnan(id(safety_temperature).state) && + id(safety_temperature).state < id(stop_temperature).state; + + if ( + !id(safety_limit) && + temp_ok && + !id(temperature_atteinte_60).state + ) { + ESP_LOGI("forced_scheduler", "Forced Scheduler ENABLED"); + id(${scheduler_unique_id}_scheduler_activate).turn_on(); + } else { + ESP_LOGI("forced_scheduler", "Forced Scheduler DISABLED"); + id(${scheduler_unique_id}_scheduler_activate).turn_off(); + } + switch: - # Track if temperature was reached during the day (persistent) + # Persistent day flag - platform: template name: "Temp atteinte dans la journée" id: temperature_atteinte_60 optimistic: true restore_mode: RESTORE_DEFAULT_OFF - # Define if scheduler is active or not + # Forced Scheduler main switch - platform: template name: "Activate ${scheduler_unique_id} Scheduler" + id: "${scheduler_unique_id}_scheduler_activate" optimistic: true restore_mode: RESTORE_DEFAULT_ON - id: "${scheduler_unique_id}_scheduler_activate" on_turn_off: then: - if: @@ -100,7 +123,6 @@ switch: - switch.turn_on: activate number: - # Scheduler Router level from 0 to 100 - platform: template name: "${scheduler_unique_id} Scheduler Router Level" id: "${scheduler_unique_id}_scheduler_router_level" @@ -112,59 +134,30 @@ number: optimistic: true mode: slider - - platform: template - name: "${scheduler_unique_id} Scheduler Checking End Threshold" - id: "${scheduler_unique_id}_scheduler_checking_end_threshold" - optimistic: true - min_value: 0 - max_value: 720 - step: 5 - mode: box - initial_value: 5 - - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Minute" - id: "${scheduler_unique_id}_scheduler_begin_min" - optimistic: true - min_value: 0 - max_value: 55 - step: 5 - mode: box - initial_value: 0 - - - platform: template - name: "${scheduler_unique_id} Scheduler End Minute" - id: "${scheduler_unique_id}_scheduler_end_min" - optimistic: true - min_value: 0 - max_value: 55 - step: 5 - mode: box - initial_value: 0 - - platform: template name: "${scheduler_unique_id} Scheduler Begin Hour" id: "${scheduler_unique_id}_scheduler_begin_hour" - optimistic: true min_value: 0 max_value: 23 step: 1 + optimistic: true mode: box initial_value: 22 - platform: template name: "${scheduler_unique_id} Scheduler End Hour" id: "${scheduler_unique_id}_scheduler_end_hour" - optimistic: true min_value: 0 max_value: 23 step: 1 + optimistic: true mode: box initial_value: 6 time: - platform: sntp id: ${scheduler_unique_id}_sntp + on_time: # Reset daily flag at 08:00 - hours: 8 @@ -173,9 +166,22 @@ time: then: - switch.turn_off: temperature_atteinte_60 - # Every minute: track day temperature + safety guard + # Decision at 19:00 + - hours: 19 + minutes: 0 + seconds: 0 + then: + - script.execute: ${scheduler_unique_id}_forced_decision + + # Every minute checks - seconds: 0 minutes: /1 then: - script.execute: ${scheduler_unique_id}_day_temperature_tracker - script.execute: ${scheduler_unique_id}_temperature_guard + +esphome: + on_boot: + priority: -100 + then: + - script.execute: ${scheduler_unique_id}_forced_decision From e063c11e5c025c1eb07e7a3cb45660f60f7b53f4 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 23:27:08 +0100 Subject: [PATCH 26/39] Update scheduler_forced_run_temperature_guard.yaml --- ...cheduler_forced_run_temperature_guard.yaml | 122 +++++++++++------- 1 file changed, 73 insertions(+), 49 deletions(-) diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml index cb0491ee..fdb0a347 100644 --- a/solar_router/scheduler_forced_run_temperature_guard.yaml +++ b/solar_router/scheduler_forced_run_temperature_guard.yaml @@ -2,18 +2,19 @@ # or when Safety limit reached is active # Track if temperature was reached during the day (08h–19h) # Variable is persistent and reset every day at 08:00 -# Forced Scheduler decision is evaluated at BOOT and at 19:00 substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script script: - # Fake empty script (compatibility) + # Fake script (unchanged) - id: ${scheduler_unique_id}_fake_script then: - # === DAY TEMPERATURE TRACKING (08h–19h) === + # ============================ + # DAY TEMPERATURE TRACKER + # ============================ - id: ${scheduler_unique_id}_day_temperature_tracker mode: single then: @@ -22,6 +23,8 @@ script: if (!now.is_valid()) return; int now_min = now.hour * 60 + now.minute; + + // Only between 08:00 and 19:00 if (now_min < 8 * 60 || now_min >= 19 * 60) return; if ( @@ -29,13 +32,17 @@ script: id(safety_temperature).state >= id(stop_temperature).state ) { if (!id(temperature_atteinte_60).state) { - ESP_LOGI("forced_scheduler", - "Temperature reached during the day → disable forced tonight"); + ESP_LOGI( + "forced_scheduler", + "Temperature reached during day -> disabling forced scheduler tonight" + ); id(temperature_atteinte_60).turn_on(); } } - # === SAFETY GUARD DURING FORCED WINDOW === + # ============================ + # TEMPERATURE / SAFETY GUARD + # ============================ - id: ${scheduler_unique_id}_temperature_guard mode: single then: @@ -44,73 +51,56 @@ script: if (!now.is_valid()) return; int now_min = now.hour * 60 + now.minute; + int begin_min = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + id(${scheduler_unique_id}_scheduler_begin_min).state; + int end_min = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; - bool in_window; - if (begin_min <= end_min) - in_window = (now_min >= begin_min && now_min < end_min); - else - in_window = (now_min >= begin_min || now_min < end_min); + bool in_scheduler_window; + if (begin_min <= end_min) { + in_scheduler_window = (now_min >= begin_min && now_min < end_min); + } else { + in_scheduler_window = (now_min >= begin_min || now_min < end_min); + } - if (!in_window) return; + if (!in_scheduler_window) return; bool temp_reached = !isnan(id(safety_temperature).state) && id(safety_temperature).state >= id(stop_temperature).state; if (id(safety_limit) || temp_reached) { - ESP_LOGW("forced_scheduler", - "STOP forced scheduler (temp=%.1f safety=%d)", + ESP_LOGW( + "forced_scheduler", + "STOP Forced Scheduler (temp=%.1f / safety=%d)", id(safety_temperature).state, id(safety_limit) ); id(${scheduler_unique_id}_scheduler_activate).turn_off(); } - # === DECIDE IF FORCED SCHEDULER SHOULD RUN === - # Executed at BOOT and at 19:00 - - id: ${scheduler_unique_id}_forced_decision - mode: single - then: - - lambda: |- - auto now = id(${scheduler_unique_id}_sntp).now(); - if (!now.is_valid()) return; - - bool temp_ok = - !isnan(id(safety_temperature).state) && - id(safety_temperature).state < id(stop_temperature).state; - - if ( - !id(safety_limit) && - temp_ok && - !id(temperature_atteinte_60).state - ) { - ESP_LOGI("forced_scheduler", "Forced Scheduler ENABLED"); - id(${scheduler_unique_id}_scheduler_activate).turn_on(); - } else { - ESP_LOGI("forced_scheduler", "Forced Scheduler DISABLED"); - id(${scheduler_unique_id}_scheduler_activate).turn_off(); - } - switch: - # Persistent day flag + # ============================ + # DAILY TEMPERATURE FLAG + # ============================ - platform: template name: "Temp atteinte dans la journée" id: temperature_atteinte_60 optimistic: true restore_mode: RESTORE_DEFAULT_OFF - # Forced Scheduler main switch + # ============================ + # FORCED SCHEDULER SWITCH + # ============================ - platform: template name: "Activate ${scheduler_unique_id} Scheduler" - id: "${scheduler_unique_id}_scheduler_activate" optimistic: true restore_mode: RESTORE_DEFAULT_ON + id: "${scheduler_unique_id}_scheduler_activate" on_turn_off: then: - if: @@ -123,6 +113,9 @@ switch: - switch.turn_on: activate number: + # ============================ + # EXISTING SCHEDULER SETTINGS + # ============================ - platform: template name: "${scheduler_unique_id} Scheduler Router Level" id: "${scheduler_unique_id}_scheduler_router_level" @@ -134,30 +127,59 @@ number: optimistic: true mode: slider + - platform: template + name: "${scheduler_unique_id} Scheduler Checking End Threshold" + id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + optimistic: true + min_value: 0 + max_value: 720 + step: 5 + mode: box + initial_value: 5 + + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: "${scheduler_unique_id}_scheduler_begin_min" + optimistic: true + min_value: 0 + max_value: 55 + step: 5 + mode: box + initial_value: 0 + + - platform: template + name: "${scheduler_unique_id} Scheduler End Minute" + id: "${scheduler_unique_id}_scheduler_end_min" + optimistic: true + min_value: 0 + max_value: 55 + step: 5 + mode: box + initial_value: 0 + - platform: template name: "${scheduler_unique_id} Scheduler Begin Hour" id: "${scheduler_unique_id}_scheduler_begin_hour" + optimistic: true min_value: 0 max_value: 23 step: 1 - optimistic: true mode: box initial_value: 22 - platform: template name: "${scheduler_unique_id} Scheduler End Hour" id: "${scheduler_unique_id}_scheduler_end_hour" + optimistic: true min_value: 0 max_value: 23 step: 1 - optimistic: true mode: box initial_value: 6 time: - platform: sntp id: ${scheduler_unique_id}_sntp - on_time: # Reset daily flag at 08:00 - hours: 8 @@ -166,14 +188,15 @@ time: then: - switch.turn_off: temperature_atteinte_60 - # Decision at 19:00 + # Force evaluation at 19:00 - hours: 19 minutes: 0 seconds: 0 then: - - script.execute: ${scheduler_unique_id}_forced_decision + - script.execute: ${scheduler_unique_id}_day_temperature_tracker + - script.execute: ${scheduler_unique_id}_temperature_guard - # Every minute checks + # Every minute - seconds: 0 minutes: /1 then: @@ -184,4 +207,5 @@ esphome: on_boot: priority: -100 then: - - script.execute: ${scheduler_unique_id}_forced_decision + - script.execute: ${scheduler_unique_id}_day_temperature_tracker + - script.execute: ${scheduler_unique_id}_temperature_guard From 1713ac655f9b9a4213ace387f00579e6f9f88850 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 23:41:16 +0100 Subject: [PATCH 27/39] Update scheduler_forced_run_temperature_guard.yaml --- ...cheduler_forced_run_temperature_guard.yaml | 90 ++++++++----------- 1 file changed, 39 insertions(+), 51 deletions(-) diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml index fdb0a347..e69367e1 100644 --- a/solar_router/scheduler_forced_run_temperature_guard.yaml +++ b/solar_router/scheduler_forced_run_temperature_guard.yaml @@ -1,20 +1,18 @@ # Stop Forced Scheduler when temperature is above stop_temperature # or when Safety limit reached is active # Track if temperature was reached during the day (08h–19h) -# Variable is persistent and reset every day at 08:00 +# Auto-enable Forced Scheduler at 19h or at boot if needed substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script script: - # Fake script (unchanged) + # Fake empty script - id: ${scheduler_unique_id}_fake_script then: - # ============================ - # DAY TEMPERATURE TRACKER - # ============================ + # === DAY TEMPERATURE TRACKER (08h–19h) === - id: ${scheduler_unique_id}_day_temperature_tracker mode: single then: @@ -23,8 +21,6 @@ script: if (!now.is_valid()) return; int now_min = now.hour * 60 + now.minute; - - // Only between 08:00 and 19:00 if (now_min < 8 * 60 || now_min >= 19 * 60) return; if ( @@ -32,17 +28,12 @@ script: id(safety_temperature).state >= id(stop_temperature).state ) { if (!id(temperature_atteinte_60).state) { - ESP_LOGI( - "forced_scheduler", - "Temperature reached during day -> disabling forced scheduler tonight" - ); + ESP_LOGI("forced_scheduler", "Day temperature reached"); id(temperature_atteinte_60).turn_on(); } } - # ============================ - # TEMPERATURE / SAFETY GUARD - # ============================ + # === SAFETY GUARD DURING SCHEDULER WINDOW === - id: ${scheduler_unique_id}_temperature_guard mode: single then: @@ -51,56 +42,58 @@ script: if (!now.is_valid()) return; int now_min = now.hour * 60 + now.minute; - int begin_min = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + id(${scheduler_unique_id}_scheduler_begin_min).state; - int end_min = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; - bool in_scheduler_window; - if (begin_min <= end_min) { - in_scheduler_window = (now_min >= begin_min && now_min < end_min); - } else { - in_scheduler_window = (now_min >= begin_min || now_min < end_min); - } + bool in_window = + (begin_min <= end_min) + ? (now_min >= begin_min && now_min < end_min) + : (now_min >= begin_min || now_min < end_min); - if (!in_scheduler_window) return; + if (!in_window) return; bool temp_reached = !isnan(id(safety_temperature).state) && id(safety_temperature).state >= id(stop_temperature).state; if (id(safety_limit) || temp_reached) { - ESP_LOGW( - "forced_scheduler", - "STOP Forced Scheduler (temp=%.1f / safety=%d)", - id(safety_temperature).state, - id(safety_limit) - ); + ESP_LOGW("forced_scheduler", "STOP Forced Scheduler"); id(${scheduler_unique_id}_scheduler_activate).turn_off(); } + # === AUTO ENABLE FORCED SCHEDULER (boot + 19h) === + - id: ${scheduler_unique_id}_auto_enable_forced + mode: single + then: + - lambda: |- + if ( + !id(temperature_atteinte_60).state && + !id(safety_limit) && + !isnan(id(safety_temperature).state) && + id(safety_temperature).state < id(stop_temperature).state + ) { + ESP_LOGI("forced_scheduler", "Auto enable Forced Scheduler"); + id(${scheduler_unique_id}_scheduler_activate).turn_on(); + } + switch: - # ============================ - # DAILY TEMPERATURE FLAG - # ============================ + # Persistent flag: temperature reached during the day - platform: template name: "Temp atteinte dans la journée" id: temperature_atteinte_60 optimistic: true restore_mode: RESTORE_DEFAULT_OFF - # ============================ - # FORCED SCHEDULER SWITCH - # ============================ + # Forced Scheduler main switch - platform: template name: "Activate ${scheduler_unique_id} Scheduler" - optimistic: true - restore_mode: RESTORE_DEFAULT_ON id: "${scheduler_unique_id}_scheduler_activate" + optimistic: true + restore_mode: RESTORE_DEFAULT_OFF on_turn_off: then: - if: @@ -113,9 +106,6 @@ switch: - switch.turn_on: activate number: - # ============================ - # EXISTING SCHEDULER SETTINGS - # ============================ - platform: template name: "${scheduler_unique_id} Scheduler Router Level" id: "${scheduler_unique_id}_scheduler_router_level" @@ -130,50 +120,50 @@ number: - platform: template name: "${scheduler_unique_id} Scheduler Checking End Threshold" id: "${scheduler_unique_id}_scheduler_checking_end_threshold" - optimistic: true min_value: 0 max_value: 720 step: 5 + optimistic: true mode: box initial_value: 5 - platform: template name: "${scheduler_unique_id} Scheduler Begin Minute" id: "${scheduler_unique_id}_scheduler_begin_min" - optimistic: true min_value: 0 max_value: 55 step: 5 + optimistic: true mode: box initial_value: 0 - platform: template name: "${scheduler_unique_id} Scheduler End Minute" id: "${scheduler_unique_id}_scheduler_end_min" - optimistic: true min_value: 0 max_value: 55 step: 5 + optimistic: true mode: box initial_value: 0 - platform: template name: "${scheduler_unique_id} Scheduler Begin Hour" id: "${scheduler_unique_id}_scheduler_begin_hour" - optimistic: true min_value: 0 max_value: 23 step: 1 + optimistic: true mode: box initial_value: 22 - platform: template name: "${scheduler_unique_id} Scheduler End Hour" id: "${scheduler_unique_id}_scheduler_end_hour" - optimistic: true min_value: 0 max_value: 23 step: 1 + optimistic: true mode: box initial_value: 6 @@ -188,15 +178,14 @@ time: then: - switch.turn_off: temperature_atteinte_60 - # Force evaluation at 19:00 + # Auto enable at 19:00 - hours: 19 minutes: 0 seconds: 0 then: - - script.execute: ${scheduler_unique_id}_day_temperature_tracker - - script.execute: ${scheduler_unique_id}_temperature_guard + - script.execute: ${scheduler_unique_id}_auto_enable_forced - # Every minute + # Every minute checks - seconds: 0 minutes: /1 then: @@ -207,5 +196,4 @@ esphome: on_boot: priority: -100 then: - - script.execute: ${scheduler_unique_id}_day_temperature_tracker - - script.execute: ${scheduler_unique_id}_temperature_guard + - script.execute: ${scheduler_unique_id}_auto_enable_forced From 2944764719d17480105e273633ec8190fe567868 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sun, 21 Dec 2025 23:51:45 +0100 Subject: [PATCH 28/39] Update scheduler_forced_run_temperature_guard.yaml --- ...cheduler_forced_run_temperature_guard.yaml | 74 ++++++++++++------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml index e69367e1..64942c3d 100644 --- a/solar_router/scheduler_forced_run_temperature_guard.yaml +++ b/solar_router/scheduler_forced_run_temperature_guard.yaml @@ -1,18 +1,19 @@ # Stop Forced Scheduler when temperature is above stop_temperature # or when Safety limit reached is active # Track if temperature was reached during the day (08h–19h) -# Auto-enable Forced Scheduler at 19h or at boot if needed +# Variable is persistent and reset every day at 08:00 +# Auto-activate Forced Scheduler when heating is really needed substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script script: - # Fake empty script + # A fake empty script to run if user don't provide a custom one - id: ${scheduler_unique_id}_fake_script then: - # === DAY TEMPERATURE TRACKER (08h–19h) === + # === DAY TEMPERATURE TRACKING === - id: ${scheduler_unique_id}_day_temperature_tracker mode: single then: @@ -33,7 +34,7 @@ script: } } - # === SAFETY GUARD DURING SCHEDULER WINDOW === + # === SAFETY / TEMPERATURE GUARD === - id: ${scheduler_unique_id}_temperature_guard mode: single then: @@ -56,44 +57,62 @@ script: if (!in_window) return; - bool temp_reached = - !isnan(id(safety_temperature).state) && - id(safety_temperature).state >= id(stop_temperature).state; - - if (id(safety_limit) || temp_reached) { + if ( + id(safety_limit) || + (!isnan(id(safety_temperature).state) && + id(safety_temperature).state >= id(stop_temperature).state) + ) { ESP_LOGW("forced_scheduler", "STOP Forced Scheduler"); id(${scheduler_unique_id}_scheduler_activate).turn_off(); } - # === AUTO ENABLE FORCED SCHEDULER (boot + 19h) === - - id: ${scheduler_unique_id}_auto_enable_forced + # === AUTO MANAGER (🔥 MISSING PIECE) === + - id: ${scheduler_unique_id}_auto_manager mode: single then: - lambda: |- + auto now = id(${scheduler_unique_id}_sntp).now(); + if (!now.is_valid()) return; + + int now_min = now.hour * 60 + now.minute; + int begin_min = + id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + int end_min = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + + bool in_window = + (begin_min <= end_min) + ? (now_min >= begin_min && now_min < end_min) + : (now_min >= begin_min || now_min < end_min); + + if (!in_window) return; + if ( - !id(temperature_atteinte_60).state && !id(safety_limit) && + !id(temperature_atteinte_60).state && !isnan(id(safety_temperature).state) && id(safety_temperature).state < id(stop_temperature).state ) { - ESP_LOGI("forced_scheduler", "Auto enable Forced Scheduler"); - id(${scheduler_unique_id}_scheduler_activate).turn_on(); + if (!id(${scheduler_unique_id}_scheduler_activate).state) { + ESP_LOGI("forced_scheduler", "AUTO → Activate Forced Scheduler"); + id(${scheduler_unique_id}_scheduler_activate).turn_on(); + } } switch: - # Persistent flag: temperature reached during the day - platform: template name: "Temp atteinte dans la journée" id: temperature_atteinte_60 optimistic: true restore_mode: RESTORE_DEFAULT_OFF - # Forced Scheduler main switch - platform: template name: "Activate ${scheduler_unique_id} Scheduler" - id: "${scheduler_unique_id}_scheduler_activate" optimistic: true - restore_mode: RESTORE_DEFAULT_OFF + restore_mode: RESTORE_DEFAULT_ON + id: "${scheduler_unique_id}_scheduler_activate" on_turn_off: then: - if: @@ -120,50 +139,50 @@ number: - platform: template name: "${scheduler_unique_id} Scheduler Checking End Threshold" id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + optimistic: true min_value: 0 max_value: 720 step: 5 - optimistic: true mode: box initial_value: 5 - platform: template name: "${scheduler_unique_id} Scheduler Begin Minute" id: "${scheduler_unique_id}_scheduler_begin_min" + optimistic: true min_value: 0 max_value: 55 step: 5 - optimistic: true mode: box initial_value: 0 - platform: template name: "${scheduler_unique_id} Scheduler End Minute" id: "${scheduler_unique_id}_scheduler_end_min" + optimistic: true min_value: 0 max_value: 55 step: 5 - optimistic: true mode: box initial_value: 0 - platform: template name: "${scheduler_unique_id} Scheduler Begin Hour" id: "${scheduler_unique_id}_scheduler_begin_hour" + optimistic: true min_value: 0 max_value: 23 step: 1 - optimistic: true mode: box initial_value: 22 - platform: template name: "${scheduler_unique_id} Scheduler End Hour" id: "${scheduler_unique_id}_scheduler_end_hour" + optimistic: true min_value: 0 max_value: 23 step: 1 - optimistic: true mode: box initial_value: 6 @@ -178,22 +197,23 @@ time: then: - switch.turn_off: temperature_atteinte_60 - # Auto enable at 19:00 + # At 19:00 → decision for the night - hours: 19 minutes: 0 seconds: 0 then: - - script.execute: ${scheduler_unique_id}_auto_enable_forced + - script.execute: ${scheduler_unique_id}_auto_manager - # Every minute checks + # Every minute - seconds: 0 minutes: /1 then: - script.execute: ${scheduler_unique_id}_day_temperature_tracker - script.execute: ${scheduler_unique_id}_temperature_guard + - script.execute: ${scheduler_unique_id}_auto_manager esphome: on_boot: priority: -100 then: - - script.execute: ${scheduler_unique_id}_auto_enable_forced + - script.execute: ${scheduler_unique_id}_auto_manager From 974f81188f59b69a4d18c5e183ddf2ab903fa3e7 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Mon, 22 Dec 2025 00:13:11 +0100 Subject: [PATCH 29/39] Update scheduler_forced_run_temperature_guard.yaml --- ...cheduler_forced_run_temperature_guard.yaml | 191 ++++++++---------- 1 file changed, 82 insertions(+), 109 deletions(-) diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml index 64942c3d..3907b3cb 100644 --- a/solar_router/scheduler_forced_run_temperature_guard.yaml +++ b/solar_router/scheduler_forced_run_temperature_guard.yaml @@ -1,9 +1,3 @@ -# Stop Forced Scheduler when temperature is above stop_temperature -# or when Safety limit reached is active -# Track if temperature was reached during the day (08h–19h) -# Variable is persistent and reset every day at 08:00 -# Auto-activate Forced Scheduler when heating is really needed - substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script @@ -13,101 +7,27 @@ script: - id: ${scheduler_unique_id}_fake_script then: - # === DAY TEMPERATURE TRACKING === - - id: ${scheduler_unique_id}_day_temperature_tracker - mode: single - then: - - lambda: |- - auto now = id(${scheduler_unique_id}_sntp).now(); - if (!now.is_valid()) return; - - int now_min = now.hour * 60 + now.minute; - if (now_min < 8 * 60 || now_min >= 19 * 60) return; - - if ( - !isnan(id(safety_temperature).state) && - id(safety_temperature).state >= id(stop_temperature).state - ) { - if (!id(temperature_atteinte_60).state) { - ESP_LOGI("forced_scheduler", "Day temperature reached"); - id(temperature_atteinte_60).turn_on(); - } - } - - # === SAFETY / TEMPERATURE GUARD === + # === SAFETY CHECK SCRIPT === + # Disable forced scheduler if temperature or safety limit is reached - id: ${scheduler_unique_id}_temperature_guard mode: single then: - lambda: |- - auto now = id(${scheduler_unique_id}_sntp).now(); - if (!now.is_valid()) return; - - int now_min = now.hour * 60 + now.minute; - int begin_min = - id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_begin_min).state; - int end_min = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - - bool in_window = - (begin_min <= end_min) - ? (now_min >= begin_min && now_min < end_min) - : (now_min >= begin_min || now_min < end_min); - - if (!in_window) return; - if ( id(safety_limit) || (!isnan(id(safety_temperature).state) && id(safety_temperature).state >= id(stop_temperature).state) ) { - ESP_LOGW("forced_scheduler", "STOP Forced Scheduler"); + ESP_LOGW("forced_scheduler", + "STOP scheduler: temp=%.1f limit=%d", + id(safety_temperature).state, + id(safety_limit) + ); id(${scheduler_unique_id}_scheduler_activate).turn_off(); } - # === AUTO MANAGER (🔥 MISSING PIECE) === - - id: ${scheduler_unique_id}_auto_manager - mode: single - then: - - lambda: |- - auto now = id(${scheduler_unique_id}_sntp).now(); - if (!now.is_valid()) return; - - int now_min = now.hour * 60 + now.minute; - int begin_min = - id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_begin_min).state; - int end_min = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - - bool in_window = - (begin_min <= end_min) - ? (now_min >= begin_min && now_min < end_min) - : (now_min >= begin_min || now_min < end_min); - - if (!in_window) return; - - if ( - !id(safety_limit) && - !id(temperature_atteinte_60).state && - !isnan(id(safety_temperature).state) && - id(safety_temperature).state < id(stop_temperature).state - ) { - if (!id(${scheduler_unique_id}_scheduler_activate).state) { - ESP_LOGI("forced_scheduler", "AUTO → Activate Forced Scheduler"); - id(${scheduler_unique_id}_scheduler_activate).turn_on(); - } - } - switch: - - platform: template - name: "Temp atteinte dans la journée" - id: temperature_atteinte_60 - optimistic: true - restore_mode: RESTORE_DEFAULT_OFF - + # Define if scheduler is active or not - platform: template name: "Activate ${scheduler_unique_id} Scheduler" optimistic: true @@ -119,12 +39,14 @@ switch: condition: - switch.is_off: activate then: + # Set router level to 0 then Enable router - number.set: id: router_level value: 0 - switch.turn_on: activate number: + # Scheduler Router level from 0 to 100 - platform: template name: "${scheduler_unique_id} Scheduler Router Level" id: "${scheduler_unique_id}_scheduler_router_level" @@ -190,30 +112,81 @@ time: - platform: sntp id: ${scheduler_unique_id}_sntp on_time: - # Reset daily flag at 08:00 - - hours: 8 - minutes: 0 - seconds: 0 - then: - - switch.turn_off: temperature_atteinte_60 - - # At 19:00 → decision for the night - - hours: 19 - minutes: 0 - seconds: 0 - then: - - script.execute: ${scheduler_unique_id}_auto_manager - # Every minute + # === Every 1 minute: temperature safety check === - seconds: 0 minutes: /1 then: - - script.execute: ${scheduler_unique_id}_day_temperature_tracker - script.execute: ${scheduler_unique_id}_temperature_guard - - script.execute: ${scheduler_unique_id}_auto_manager -esphome: - on_boot: - priority: -100 - then: - - script.execute: ${scheduler_unique_id}_auto_manager + # === Existing scheduler logic (unchanged) === + - seconds: 0 + minutes: /5 + then: + - if: + condition: + - switch.is_on: ${scheduler_unique_id}_scheduler_activate + then: + - if: + condition: + lambda: |- + int beginTotalMinutes = + id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_begin_min).state; + int endTotalMinutes = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int checkTotalMinutes = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && + checkTotalMinutes < endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || + checkTotalMinutes < endTotalMinutes; + } + then: + - switch.turn_off: activate + - number.set: + id: router_level + value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; + - script.execute: ${custom_script} + else: + - if: + condition: + and: + - switch.is_off: activate + - lambda: |- + if ( + id(${scheduler_unique_id}_scheduler_end_hour).state == + id(${scheduler_unique_id}_sntp).now().hour && + id(${scheduler_unique_id}_scheduler_end_min).state == + id(${scheduler_unique_id}_sntp).now().minute + ) { + return true; + } + + int beginTotalMinutes = + id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + + id(${scheduler_unique_id}_scheduler_end_min).state; + int endTotalMinutes = + beginTotalMinutes + + id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; + int checkTotalMinutes = + id(${scheduler_unique_id}_sntp).now().hour * 60 + + id(${scheduler_unique_id}_sntp).now().minute; + + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && + checkTotalMinutes <= endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || + checkTotalMinutes <= endTotalMinutes; + } + then: + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate From 259630884b6e6a54f2bd1b4f958fce16dd939560 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Mon, 22 Dec 2025 12:28:24 +0100 Subject: [PATCH 30/39] Delete solar_router/scheduler_forced_run_temperature_guard.yaml --- ...cheduler_forced_run_temperature_guard.yaml | 192 ------------------ 1 file changed, 192 deletions(-) delete mode 100644 solar_router/scheduler_forced_run_temperature_guard.yaml diff --git a/solar_router/scheduler_forced_run_temperature_guard.yaml b/solar_router/scheduler_forced_run_temperature_guard.yaml deleted file mode 100644 index 3907b3cb..00000000 --- a/solar_router/scheduler_forced_run_temperature_guard.yaml +++ /dev/null @@ -1,192 +0,0 @@ -substitutions: - scheduler_unique_id: "Forced" - custom_script: ${scheduler_unique_id}_fake_script - -script: - # A fake empty script to run if user don't provide a custom one - - id: ${scheduler_unique_id}_fake_script - then: - - # === SAFETY CHECK SCRIPT === - # Disable forced scheduler if temperature or safety limit is reached - - id: ${scheduler_unique_id}_temperature_guard - mode: single - then: - - lambda: |- - if ( - id(safety_limit) || - (!isnan(id(safety_temperature).state) && - id(safety_temperature).state >= id(stop_temperature).state) - ) { - ESP_LOGW("forced_scheduler", - "STOP scheduler: temp=%.1f limit=%d", - id(safety_temperature).state, - id(safety_limit) - ); - id(${scheduler_unique_id}_scheduler_activate).turn_off(); - } - -switch: - # Define if scheduler is active or not - - platform: template - name: "Activate ${scheduler_unique_id} Scheduler" - optimistic: true - restore_mode: RESTORE_DEFAULT_ON - id: "${scheduler_unique_id}_scheduler_activate" - on_turn_off: - then: - - if: - condition: - - switch.is_off: activate - then: - # Set router level to 0 then Enable router - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate - -number: - # Scheduler Router level from 0 to 100 - - platform: template - name: "${scheduler_unique_id} Scheduler Router Level" - id: "${scheduler_unique_id}_scheduler_router_level" - min_value: 0 - max_value: 100 - initial_value: 100 - step: 1 - unit_of_measurement: "%" - optimistic: true - mode: slider - - - platform: template - name: "${scheduler_unique_id} Scheduler Checking End Threshold" - id: "${scheduler_unique_id}_scheduler_checking_end_threshold" - optimistic: true - min_value: 0 - max_value: 720 - step: 5 - mode: box - initial_value: 5 - - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Minute" - id: "${scheduler_unique_id}_scheduler_begin_min" - optimistic: true - min_value: 0 - max_value: 55 - step: 5 - mode: box - initial_value: 0 - - - platform: template - name: "${scheduler_unique_id} Scheduler End Minute" - id: "${scheduler_unique_id}_scheduler_end_min" - optimistic: true - min_value: 0 - max_value: 55 - step: 5 - mode: box - initial_value: 0 - - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Hour" - id: "${scheduler_unique_id}_scheduler_begin_hour" - optimistic: true - min_value: 0 - max_value: 23 - step: 1 - mode: box - initial_value: 22 - - - platform: template - name: "${scheduler_unique_id} Scheduler End Hour" - id: "${scheduler_unique_id}_scheduler_end_hour" - optimistic: true - min_value: 0 - max_value: 23 - step: 1 - mode: box - initial_value: 6 - -time: - - platform: sntp - id: ${scheduler_unique_id}_sntp - on_time: - - # === Every 1 minute: temperature safety check === - - seconds: 0 - minutes: /1 - then: - - script.execute: ${scheduler_unique_id}_temperature_guard - - # === Existing scheduler logic (unchanged) === - - seconds: 0 - minutes: /5 - then: - - if: - condition: - - switch.is_on: ${scheduler_unique_id}_scheduler_activate - then: - - if: - condition: - lambda: |- - int beginTotalMinutes = - id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_begin_min).state; - int endTotalMinutes = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - int checkTotalMinutes = - id(${scheduler_unique_id}_sntp).now().hour * 60 + - id(${scheduler_unique_id}_sntp).now().minute; - - if (beginTotalMinutes <= endTotalMinutes) { - return checkTotalMinutes >= beginTotalMinutes && - checkTotalMinutes < endTotalMinutes; - } else { - return checkTotalMinutes >= beginTotalMinutes || - checkTotalMinutes < endTotalMinutes; - } - then: - - switch.turn_off: activate - - number.set: - id: router_level - value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; - - script.execute: ${custom_script} - else: - - if: - condition: - and: - - switch.is_off: activate - - lambda: |- - if ( - id(${scheduler_unique_id}_scheduler_end_hour).state == - id(${scheduler_unique_id}_sntp).now().hour && - id(${scheduler_unique_id}_scheduler_end_min).state == - id(${scheduler_unique_id}_sntp).now().minute - ) { - return true; - } - - int beginTotalMinutes = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - int endTotalMinutes = - beginTotalMinutes + - id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; - int checkTotalMinutes = - id(${scheduler_unique_id}_sntp).now().hour * 60 + - id(${scheduler_unique_id}_sntp).now().minute; - - if (beginTotalMinutes <= endTotalMinutes) { - return checkTotalMinutes >= beginTotalMinutes && - checkTotalMinutes <= endTotalMinutes; - } else { - return checkTotalMinutes >= beginTotalMinutes || - checkTotalMinutes <= endTotalMinutes; - } - then: - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate From b06f9e0bb830b905808128e42442267152fa737b Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Mon, 22 Dec 2025 12:28:39 +0100 Subject: [PATCH 31/39] Delete solar_router/scheduler_forced_run_safe_beta.yaml --- .../scheduler_forced_run_safe_beta.yaml | 193 ------------------ 1 file changed, 193 deletions(-) delete mode 100644 solar_router/scheduler_forced_run_safe_beta.yaml diff --git a/solar_router/scheduler_forced_run_safe_beta.yaml b/solar_router/scheduler_forced_run_safe_beta.yaml deleted file mode 100644 index 2c79ede9..00000000 --- a/solar_router/scheduler_forced_run_safe_beta.yaml +++ /dev/null @@ -1,193 +0,0 @@ -#stop scheduler when temp is above stop_temperature or Safety limit reached is activated -substitutions: - scheduler_unique_id: "Forced" - custom_script: ${scheduler_unique_id}_fake_script - -script: - # A fake empty script to run if user don't provide a custom one - - id: ${scheduler_unique_id}_fake_script - then: - - # === SAFETY CHECK SCRIPT === - # Disable forced scheduler if temperature or safety limit is reached - - id: ${scheduler_unique_id}_temperature_guard - mode: single - then: - - lambda: |- - if ( - id(safety_limit) || - (!isnan(id(safety_temperature).state) && - id(safety_temperature).state >= id(stop_temperature).state) - ) { - ESP_LOGW("forced_scheduler", - "STOP scheduler: temp=%.1f limit=%d", - id(safety_temperature).state, - id(safety_limit) - ); - id(${scheduler_unique_id}_scheduler_activate).turn_off(); - } - -switch: - # Define if scheduler is active or not - - platform: template - name: "Activate ${scheduler_unique_id} Scheduler" - optimistic: true - restore_mode: RESTORE_DEFAULT_ON - id: "${scheduler_unique_id}_scheduler_activate" - on_turn_off: - then: - - if: - condition: - - switch.is_off: activate - then: - # Set router level to 0 then Enable router - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate - -number: - # Scheduler Router level from 0 to 100 - - platform: template - name: "${scheduler_unique_id} Scheduler Router Level" - id: "${scheduler_unique_id}_scheduler_router_level" - min_value: 0 - max_value: 100 - initial_value: 100 - step: 1 - unit_of_measurement: "%" - optimistic: true - mode: slider - - - platform: template - name: "${scheduler_unique_id} Scheduler Checking End Threshold" - id: "${scheduler_unique_id}_scheduler_checking_end_threshold" - optimistic: true - min_value: 0 - max_value: 720 - step: 5 - mode: box - initial_value: 5 - - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Minute" - id: "${scheduler_unique_id}_scheduler_begin_min" - optimistic: true - min_value: 0 - max_value: 55 - step: 5 - mode: box - initial_value: 0 - - - platform: template - name: "${scheduler_unique_id} Scheduler End Minute" - id: "${scheduler_unique_id}_scheduler_end_min" - optimistic: true - min_value: 0 - max_value: 55 - step: 5 - mode: box - initial_value: 0 - - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Hour" - id: "${scheduler_unique_id}_scheduler_begin_hour" - optimistic: true - min_value: 0 - max_value: 23 - step: 1 - mode: box - initial_value: 22 - - - platform: template - name: "${scheduler_unique_id} Scheduler End Hour" - id: "${scheduler_unique_id}_scheduler_end_hour" - optimistic: true - min_value: 0 - max_value: 23 - step: 1 - mode: box - initial_value: 6 - -time: - - platform: sntp - id: ${scheduler_unique_id}_sntp - on_time: - - # === Every 1 minute: temperature safety check === - - seconds: 0 - minutes: /1 - then: - - script.execute: ${scheduler_unique_id}_temperature_guard - - # === Existing scheduler logic (unchanged) === - - seconds: 0 - minutes: /5 - then: - - if: - condition: - - switch.is_on: ${scheduler_unique_id}_scheduler_activate - then: - - if: - condition: - lambda: |- - int beginTotalMinutes = - id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_begin_min).state; - int endTotalMinutes = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - int checkTotalMinutes = - id(${scheduler_unique_id}_sntp).now().hour * 60 + - id(${scheduler_unique_id}_sntp).now().minute; - - if (beginTotalMinutes <= endTotalMinutes) { - return checkTotalMinutes >= beginTotalMinutes && - checkTotalMinutes < endTotalMinutes; - } else { - return checkTotalMinutes >= beginTotalMinutes || - checkTotalMinutes < endTotalMinutes; - } - then: - - switch.turn_off: activate - - number.set: - id: router_level - value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; - - script.execute: ${custom_script} - else: - - if: - condition: - and: - - switch.is_off: activate - - lambda: |- - if ( - id(${scheduler_unique_id}_scheduler_end_hour).state == - id(${scheduler_unique_id}_sntp).now().hour && - id(${scheduler_unique_id}_scheduler_end_min).state == - id(${scheduler_unique_id}_sntp).now().minute - ) { - return true; - } - - int beginTotalMinutes = - id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + - id(${scheduler_unique_id}_scheduler_end_min).state; - int endTotalMinutes = - beginTotalMinutes + - id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; - int checkTotalMinutes = - id(${scheduler_unique_id}_sntp).now().hour * 60 + - id(${scheduler_unique_id}_sntp).now().minute; - - if (beginTotalMinutes <= endTotalMinutes) { - return checkTotalMinutes >= beginTotalMinutes && - checkTotalMinutes <= endTotalMinutes; - } else { - return checkTotalMinutes >= beginTotalMinutes || - checkTotalMinutes <= endTotalMinutes; - } - then: - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate From 44ca7870196e5b284cfc3ecd3335d8416c6463dd Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Mon, 22 Dec 2025 12:28:51 +0100 Subject: [PATCH 32/39] Delete solar_router/scheduler_forced_run_safe.yaml --- solar_router/scheduler_forced_run_safe.yaml | 149 -------------------- 1 file changed, 149 deletions(-) delete mode 100644 solar_router/scheduler_forced_run_safe.yaml diff --git a/solar_router/scheduler_forced_run_safe.yaml b/solar_router/scheduler_forced_run_safe.yaml deleted file mode 100644 index 8a66eae0..00000000 --- a/solar_router/scheduler_forced_run_safe.yaml +++ /dev/null @@ -1,149 +0,0 @@ -substitutions: - scheduler_unique_id: "Forced" - custom_script: ${scheduler_unique_id}_fake_script - -script: - # A fake empty script to run if user don't provide a custom one - - id: ${scheduler_unique_id}_fake_script - then: - -switch: - # Define is scheduler is active or not - - platform: template - name: "Activate ${scheduler_unique_id} Scheduler" - optimistic: true - restore_mode: RESTORE_DEFAULT_ON - id: "${scheduler_unique_id}_scheduler_activate" - on_turn_off: - then: - - if: - condition: - - switch.is_off: activate - then: - # Set router level to 0 then Enable router - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate - -number: - # Scheduler Router level from 0 to 100 - # When solar routing is disabled at scheduled time: Acts as a manual control to set the router level - - platform: template - name: "${scheduler_unique_id} Scheduler Router Level" - id: "${scheduler_unique_id}_scheduler_router_level" - min_value: 0 - max_value: 100 - initial_value: 100 - step: 1 - unit_of_measurement: "%" - optimistic: True - mode: slider - - platform: template - name: "${scheduler_unique_id} Scheduler Checking End Threshold" - id: "${scheduler_unique_id}_scheduler_checking_end_threshold" - optimistic: True - min_value: 0 - max_value: 720 #12 hours max - step: 5 - mode: box - initial_value: 5 - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Minute" - id: "${scheduler_unique_id}_scheduler_begin_min" - optimistic: True - min_value: 0 - max_value: 55 - step: 5 - mode: box - initial_value: 0 - - platform: template - name: "${scheduler_unique_id} Scheduler End Minute" - id: "${scheduler_unique_id}_scheduler_end_min" - optimistic: True - min_value: 0 - max_value: 55 - step: 5 - mode: box - initial_value: 0 - - platform: template - name: "${scheduler_unique_id} Scheduler Begin Hour" - id: "${scheduler_unique_id}_scheduler_begin_hour" - optimistic: True - min_value: 0 - max_value: 23 - step: 1 - mode: box - initial_value: 22 - - platform: template - name: "${scheduler_unique_id} Scheduler End Hour" - id: "${scheduler_unique_id}_scheduler_end_hour" - optimistic: True - min_value: 0 - max_value: 23 - step: 1 - mode: box - initial_value: 6 - -time: - - platform: sntp - id: ${scheduler_unique_id}_sntp - on_time: - - - seconds: 0 - minutes: /5 - then: - - if: - condition: - - switch.is_on: ${scheduler_unique_id}_scheduler_activate - then: - - if: - condition: - lambda: |- - // We are between begin (included) and end (excluded) hour and minutes - int beginTotalMinutes = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + id(${scheduler_unique_id}_scheduler_begin_min).state; - int endTotalMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; - int checkTotalMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 + id(${scheduler_unique_id}_sntp).now().minute; - - if (beginTotalMinutes <= endTotalMinutes) { - return checkTotalMinutes >= beginTotalMinutes && checkTotalMinutes < endTotalMinutes; - } else { - return checkTotalMinutes >= beginTotalMinutes || checkTotalMinutes < endTotalMinutes; - } - then: - # Disable Router then set router level to X percent - - switch.turn_off: activate - - number.set: - id: router_level - value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; - # if deactivation script option - - script.execute: ${custom_script} - else: - - if: - condition: - and: - - switch.is_off: activate - # End Hour and minutes are reached - - lambda: |- - // End Hour and minutes are reached - if (id(${scheduler_unique_id}_scheduler_end_hour).state == id(${scheduler_unique_id}_sntp).now().hour && id(${scheduler_unique_id}_scheduler_end_min).state == id(${scheduler_unique_id}_sntp).now().minute){ - return true; - } - - // Or we are between end hour and minutes and scheduler_checking_end_threshold - // It's a failsafe to check that router have been activated even if we lost the moment of end hour and minutes - int beginTotalMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; - int endTotalMinutes = beginTotalMinutes + id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; - int checkTotalMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 + id(${scheduler_unique_id}_sntp).now().minute; - - if (beginTotalMinutes <= endTotalMinutes) { - return checkTotalMinutes >= beginTotalMinutes && checkTotalMinutes <= endTotalMinutes; - } else { - return checkTotalMinutes >= beginTotalMinutes || checkTotalMinutes <= endTotalMinutes; - } - then: - # Set router level to 0 then Enable router - - number.set: - id: router_level - value: 0 - - switch.turn_on: activate From e3acc740aaa0a76c81c101066d7daff608c4de4f Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Mon, 22 Dec 2025 12:32:45 +0100 Subject: [PATCH 33/39] Create scheduler_forced_run_tempo.yaml --- solar_router/scheduler_forced_run_tempo.yaml | 150 +++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 solar_router/scheduler_forced_run_tempo.yaml diff --git a/solar_router/scheduler_forced_run_tempo.yaml b/solar_router/scheduler_forced_run_tempo.yaml new file mode 100644 index 00000000..8986d73e --- /dev/null +++ b/solar_router/scheduler_forced_run_tempo.yaml @@ -0,0 +1,150 @@ +#et tempo 22h - 00h initial_value and scheduler checking treshold initial_value: 5 +substitutions: + scheduler_unique_id: "Forced" + custom_script: ${scheduler_unique_id}_fake_script + +script: + # A fake empty script to run if user don't provide a custom one + - id: ${scheduler_unique_id}_fake_script + then: + +switch: + # Define is scheduler is active or not + - platform: template + name: "Activate ${scheduler_unique_id} Scheduler" + optimistic: true + restore_mode: RESTORE_DEFAULT_ON + id: "${scheduler_unique_id}_scheduler_activate" + on_turn_off: + then: + - if: + condition: + - switch.is_off: activate + then: + # Set router level to 0 then Enable router + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate + +number: + # Scheduler Router level from 0 to 100 + # When solar routing is disabled at scheduled time: Acts as a manual control to set the router level + - platform: template + name: "${scheduler_unique_id} Scheduler Router Level" + id: "${scheduler_unique_id}_scheduler_router_level" + min_value: 0 + max_value: 100 + initial_value: 100 + step: 1 + unit_of_measurement: "%" + optimistic: True + mode: slider + - platform: template + name: "${scheduler_unique_id} Scheduler Checking End Threshold" + id: "${scheduler_unique_id}_scheduler_checking_end_threshold" + optimistic: True + min_value: 0 + max_value: 720 #12 hours max + step: 5 + mode: box + initial_value: 5 + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Minute" + id: "${scheduler_unique_id}_scheduler_begin_min" + optimistic: True + min_value: 0 + max_value: 55 + step: 5 + mode: box + initial_value: 0 + - platform: template + name: "${scheduler_unique_id} Scheduler End Minute" + id: "${scheduler_unique_id}_scheduler_end_min" + optimistic: True + min_value: 0 + max_value: 55 + step: 5 + mode: box + initial_value: 0 + - platform: template + name: "${scheduler_unique_id} Scheduler Begin Hour" + id: "${scheduler_unique_id}_scheduler_begin_hour" + optimistic: True + min_value: 0 + max_value: 23 + step: 1 + mode: box + initial_value: 22 + - platform: template + name: "${scheduler_unique_id} Scheduler End Hour" + id: "${scheduler_unique_id}_scheduler_end_hour" + optimistic: True + min_value: 0 + max_value: 23 + step: 1 + mode: box + initial_value: 6 + +time: + - platform: sntp + id: ${scheduler_unique_id}_sntp + on_time: + + - seconds: 0 + minutes: /5 + then: + - if: + condition: + - switch.is_on: ${scheduler_unique_id}_scheduler_activate + then: + - if: + condition: + lambda: |- + // We are between begin (included) and end (excluded) hour and minutes + int beginTotalMinutes = id(${scheduler_unique_id}_scheduler_begin_hour).state * 60 + id(${scheduler_unique_id}_scheduler_begin_min).state; + int endTotalMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; + int checkTotalMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 + id(${scheduler_unique_id}_sntp).now().minute; + + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && checkTotalMinutes < endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || checkTotalMinutes < endTotalMinutes; + } + then: + # Disable Router then set router level to X percent + - switch.turn_off: activate + - number.set: + id: router_level + value: !lambda return id(${scheduler_unique_id}_scheduler_router_level).state; + # if deactivation script option + - script.execute: ${custom_script} + else: + - if: + condition: + and: + - switch.is_off: activate + # End Hour and minutes are reached + - lambda: |- + // End Hour and minutes are reached + if (id(${scheduler_unique_id}_scheduler_end_hour).state == id(${scheduler_unique_id}_sntp).now().hour && id(${scheduler_unique_id}_scheduler_end_min).state == id(${scheduler_unique_id}_sntp).now().minute){ + return true; + } + + // Or we are between end hour and minutes and scheduler_checking_end_threshold + // It's a failsafe to check that router have been activated even if we lost the moment of end hour and minutes + int beginTotalMinutes = id(${scheduler_unique_id}_scheduler_end_hour).state * 60 + id(${scheduler_unique_id}_scheduler_end_min).state; + int endTotalMinutes = beginTotalMinutes + id(${scheduler_unique_id}_scheduler_checking_end_threshold).state; + int checkTotalMinutes = id(${scheduler_unique_id}_sntp).now().hour * 60 + id(${scheduler_unique_id}_sntp).now().minute; + + if (beginTotalMinutes <= endTotalMinutes) { + return checkTotalMinutes >= beginTotalMinutes && checkTotalMinutes <= endTotalMinutes; + } else { + return checkTotalMinutes >= beginTotalMinutes || checkTotalMinutes <= endTotalMinutes; + } + then: + # Set router level to 0 then Enable router + - number.set: + id: router_level + value: 0 + - switch.turn_on: activate From e54c075e9bb1a10192d7fda7e866f5ebf185cae2 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:11:22 +0100 Subject: [PATCH 34/39] Update scheduler_forced_run_tempo.yaml --- solar_router/scheduler_forced_run_tempo.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solar_router/scheduler_forced_run_tempo.yaml b/solar_router/scheduler_forced_run_tempo.yaml index 8986d73e..c7b3cf42 100644 --- a/solar_router/scheduler_forced_run_tempo.yaml +++ b/solar_router/scheduler_forced_run_tempo.yaml @@ -1,4 +1,4 @@ -#et tempo 22h - 00h initial_value and scheduler checking treshold initial_value: 5 +#edf tempo 22h - 00h initial_value and scheduler checking treshold initial_value: 5 substitutions: scheduler_unique_id: "Forced" custom_script: ${scheduler_unique_id}_fake_script From 13d2c97cf863fdb9c6786d7c84d3f4134212098f Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:14:11 +0100 Subject: [PATCH 35/39] Create regulator_solid_state_relay_dev.yaml --- .../regulator_solid_state_relay_dev.yaml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 solar_router/solar_router/regulator_solid_state_relay_dev.yaml diff --git a/solar_router/solar_router/regulator_solid_state_relay_dev.yaml b/solar_router/solar_router/regulator_solid_state_relay_dev.yaml new file mode 100644 index 00000000..6634b0db --- /dev/null +++ b/solar_router/solar_router/regulator_solid_state_relay_dev.yaml @@ -0,0 +1,25 @@ +# ---------------------------------------------------------------------------------------------------- +# Define scripts for energy divertion +# ---------------------------------------------------------------------------------------------------- + +script: + # Apply regulation on relay + - id: regulation_control + mode: single + then: + # Apply opening level on relay ldec output + - output.turn_on: ssr_output + - output.set_level: + id: ssr_output + level: !lambda return id(regulator_opening).state/100.0; + +# ---------------------------------------------------------------------------------------------------- +# relay control +# ---------------------------------------------------------------------------------------------------- + +# Control the relay through GPIO +output: + - platform: slow_pwm + id: ssr_output + pin: ${regulator_gate_pin} + period: 20ms From 4940412e7bb8b979d603ce496af167cd1aa72f93 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:17:47 +0100 Subject: [PATCH 36/39] Delete solar_router/solar_router directory --- .../regulator_solid_state_relay_dev.yaml | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 solar_router/solar_router/regulator_solid_state_relay_dev.yaml diff --git a/solar_router/solar_router/regulator_solid_state_relay_dev.yaml b/solar_router/solar_router/regulator_solid_state_relay_dev.yaml deleted file mode 100644 index 6634b0db..00000000 --- a/solar_router/solar_router/regulator_solid_state_relay_dev.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# ---------------------------------------------------------------------------------------------------- -# Define scripts for energy divertion -# ---------------------------------------------------------------------------------------------------- - -script: - # Apply regulation on relay - - id: regulation_control - mode: single - then: - # Apply opening level on relay ldec output - - output.turn_on: ssr_output - - output.set_level: - id: ssr_output - level: !lambda return id(regulator_opening).state/100.0; - -# ---------------------------------------------------------------------------------------------------- -# relay control -# ---------------------------------------------------------------------------------------------------- - -# Control the relay through GPIO -output: - - platform: slow_pwm - id: ssr_output - pin: ${regulator_gate_pin} - period: 20ms From e7f463c9956ab40d380bc6b43e534d4e35fe1718 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:18:37 +0100 Subject: [PATCH 37/39] Create regulator_solid_state_relay_dev.yaml --- .../regulator_solid_state_relay_dev.yaml | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 solar_router/regulator_solid_state_relay_dev.yaml diff --git a/solar_router/regulator_solid_state_relay_dev.yaml b/solar_router/regulator_solid_state_relay_dev.yaml new file mode 100644 index 00000000..6634b0db --- /dev/null +++ b/solar_router/regulator_solid_state_relay_dev.yaml @@ -0,0 +1,25 @@ +# ---------------------------------------------------------------------------------------------------- +# Define scripts for energy divertion +# ---------------------------------------------------------------------------------------------------- + +script: + # Apply regulation on relay + - id: regulation_control + mode: single + then: + # Apply opening level on relay ldec output + - output.turn_on: ssr_output + - output.set_level: + id: ssr_output + level: !lambda return id(regulator_opening).state/100.0; + +# ---------------------------------------------------------------------------------------------------- +# relay control +# ---------------------------------------------------------------------------------------------------- + +# Control the relay through GPIO +output: + - platform: slow_pwm + id: ssr_output + pin: ${regulator_gate_pin} + period: 20ms From 4127719c3fd33c32b7a0c720d79e5f017b5cfd87 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Fri, 30 Jan 2026 12:20:09 +0100 Subject: [PATCH 38/39] Update regulator_solid_state_relay_dev.yaml --- solar_router/regulator_solid_state_relay_dev.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solar_router/regulator_solid_state_relay_dev.yaml b/solar_router/regulator_solid_state_relay_dev.yaml index 6634b0db..bf8f7ea5 100644 --- a/solar_router/regulator_solid_state_relay_dev.yaml +++ b/solar_router/regulator_solid_state_relay_dev.yaml @@ -22,4 +22,4 @@ output: - platform: slow_pwm id: ssr_output pin: ${regulator_gate_pin} - period: 20ms + period: 100ms From c2185fa0cb5d4b987c1b4ad0203253064496a972 Mon Sep 17 00:00:00 2001 From: famascl3m <40993037+famascl3m@users.noreply.github.com> Date: Sat, 7 Feb 2026 15:17:51 +0100 Subject: [PATCH 39/39] Create power_meter_shelly_em_pro_tri.yaml --- .../power_meter_shelly_em_pro_tri.yaml | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 solar_router/power_meter_shelly_em_pro_tri.yaml diff --git a/solar_router/power_meter_shelly_em_pro_tri.yaml b/solar_router/power_meter_shelly_em_pro_tri.yaml new file mode 100644 index 00000000..e8440b65 --- /dev/null +++ b/solar_router/power_meter_shelly_em_pro_tri.yaml @@ -0,0 +1,63 @@ +<<: !include power_meter_common.yaml + +esphome: + min_version: 2025.5.0 + +http_request: + id: http_request_data + useragent: esphome/device + timeout: 10s + verify_ssl: false + +script: + - id: power_meter_source + mode: single + then: + - if: + condition: + lambda: return network::is_connected(); + then: + - http_request.get: + url: http://${power_meter_ip_address}/rpc/Shelly.GetStatus + request_headers: + Content-Type: application/json + Authorization: ${power_meter_auth_header} + capture_response: true + max_response_buffer_size: 4096 + on_response: + then: + - lambda: |- + if (response->status_code != 200) { + ESP_LOGW("shelly", "HTTP error %d", response->status_code); + id(real_power).publish_state(NAN); + return; + } + bool ok = json::parse_json(body, [](JsonObject root) -> bool { + // Pour Shelly EM Pro tri, la clé est "em:0" au lieu de "em1:X" + if (!root["em:0"]["total_act_power"].is()) { + ESP_LOGW("shelly", "total_act_power missing for em:0"); + return false; + } + float power = root["em:0"]["total_act_power"].as(); + id(real_power).publish_state(power); + return true; + }); + if (!ok) { + id(real_power).publish_state(NAN); + } + on_error: + then: + - lambda: |- + ESP_LOGW("shelly", "HTTP request failed"); + id(real_power).publish_state(NAN); + +time: + - platform: sntp + on_time: + - seconds: /1 + then: + - if: + condition: + lambda: return id(power_meter_activated) != 0; + then: + - script.execute: power_meter_source