diff --git a/scheduler_forced_run_temperature_guard.yaml b/scheduler_forced_run_temperature_guard.yaml new file mode 100644 index 0000000..3d8831a --- /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 diff --git a/solar_router/power_meter_shelly_em_pro.yaml b/solar_router/power_meter_shelly_em_pro.yaml new file mode 100644 index 0000000..c1e0c42 --- /dev/null +++ b/solar_router/power_meter_shelly_em_pro.yaml @@ -0,0 +1,68 @@ +<<: !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 { + 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); + return false; + } + + float power = root[key]["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 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 0000000..e8440b6 --- /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 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 0000000..bf8f7ea --- /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: 100ms diff --git a/solar_router/scheduler_forced_run_tempo.yaml b/solar_router/scheduler_forced_run_tempo.yaml new file mode 100644 index 0000000..c7b3cf4 --- /dev/null +++ b/solar_router/scheduler_forced_run_tempo.yaml @@ -0,0 +1,150 @@ +#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 + +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