Last active
August 26, 2025 04:23
-
-
Save JohanAlvedal/56e06d6b7b62cf4a080bb04d0e5b015f to your computer and use it in GitHub Desktop.
Predbat Charging & Discharging Control (Huawei Solar)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# ============================= | |
# Blueprint — Predbat Charging & Discharging Control (Huawei Solar) | |
# with optional hysteresis (start/stop) + cooldown | |
# ============================= | |
blueprint: | |
name: Predbat Charging & Discharging Control (Huawei Solar) — hysteresis + cooldown | |
description: | | |
Adjustable hysteresis and cooldown with a master switch: | |
• enable_hysteresis: turn ON/OFF hysteresis + cooldown | |
• hysteresis_start_pct: start charging only if SoC <= (target - start) | |
• hysteresis_stop_pct: stop ongoing charging if SoC >= (target - stop) | |
• hold_cooldown_min: minimum stability time after Hold/Freeze/Demand | |
Assumptions (Predbat defaults): | |
• predbat.status | |
• binary_sensor.predbat_charging | |
• binary_sensor.predbat_exporting | |
• predbat.best_charge_limit (fallback: predbat.charge_limit) | |
• predbat.best_export_limit | |
• switch.predbat_set_read_only | |
• binary_sensor.predbat_car_charging_slot | |
Huawei Solar: | |
• number.*battery_maximum_charging_power | |
• number.*battery_maximum_discharging_power | |
• huawei_solar.forcible_charge_soc | |
• huawei_solar.forcible_discharge_soc | |
• huawei_solar.forcible_discharge | |
• huawei_solar.stop_forcible_charge | |
domain: automation | |
input: | |
# Battery device_id (text) | |
battery_device_id: | |
name: Huawei battery device_id (Connected Energy Storage) | |
description: Paste the device_id for the battery device (NOT the inverter) | |
selector: | |
text: {} | |
# SoC sensor | |
soc_sensor: | |
name: Battery SoC sensor | |
description: Sensor that reports State of Capacity (SoC) in %. | |
selector: | |
entity: | |
domain: sensor | |
# Numbers to control | |
number_charging_power: | |
name: Number — battery_maximum_charging_power | |
selector: | |
entity: | |
domain: number | |
number_discharging_power: | |
name: Number — battery_maximum_discharging_power | |
selector: | |
entity: | |
domain: number | |
# Time windows & powers (W) | |
day_start: | |
name: Day window start | |
selector: { time: {} } | |
default: "06:00:00" | |
day_end: | |
name: Day window end | |
selector: { time: {} } | |
default: "23:30:00" | |
day_charge_power: | |
name: Day — charging (W) | |
selector: { number: { min: 0, max: 5000, step: 100, mode: slider } } | |
default: 4500 | |
day_car_charge_power: | |
name: Day (car slot ON) — charging (W) | |
selector: { number: { min: 0, max: 5000, step: 100, mode: slider } } | |
default: 2500 | |
night_charge_power: | |
name: Night — charging (W) | |
selector: { number: { min: 0, max: 5000, step: 100, mode: slider } } | |
default: 5000 | |
night_car_charge_power: | |
name: Night (car slot ON) — charging (W) | |
selector: { number: { min: 0, max: 5000, step: 100, mode: slider } } | |
default: 2500 | |
discharge_power: | |
name: Discharging (W) | |
selector: { number: { min: 0, max: 5000, step: 100, mode: slider } } | |
default: 5000 | |
# --- Master switch + hysteresis params --- | |
enable_hysteresis: | |
name: Enable hysteresis & cooldown | |
selector: { boolean: {} } | |
default: true | |
hysteresis_start_pct: | |
name: Hysteresis — start (percentage points) | |
description: Start charging only if SoC <= (target_soc - start). | |
default: 3.0 | |
selector: { number: { min: 0, max: 5, step: 0.1, mode: slider } } | |
hysteresis_stop_pct: | |
name: Hysteresis — stop (percentage points) | |
description: Stop charging if SoC >= (target_soc - stop). | |
default: 1.0 | |
selector: { number: { min: 0, max: 5, step: 0.1, mode: slider } } | |
hold_cooldown_min: | |
name: Hold/Freeze cooldown (minutes) | |
description: Block new charge start shortly after mode changes. | |
default: 10 | |
selector: { number: { min: 0, max: 60, step: 1 } } | |
mode: restart | |
max_exceeded: silent | |
# ---- Triggers ---- | |
trigger: | |
- id: predbat_export_on | |
platform: state | |
entity_id: binary_sensor.predbat_exporting | |
to: 'on' | |
for: '00:00:03' | |
- id: predbat_charging_on | |
platform: state | |
entity_id: binary_sensor.predbat_charging | |
to: 'on' | |
for: '00:00:03' | |
- id: charge_limit_changes_best | |
platform: state | |
entity_id: predbat.best_charge_limit | |
for: '00:00:03' | |
- id: charge_limit_changes | |
platform: state | |
entity_id: predbat.charge_limit | |
for: '00:00:03' | |
- id: status_changes | |
platform: state | |
entity_id: predbat.status | |
for: '00:00:03' | |
# ---- Safety ---- | |
condition: | |
- condition: state | |
entity_id: switch.predbat_set_read_only | |
state: 'off' | |
# ---- Variables (used inside templates) ---- | |
variables: | |
soc_sensor_e: !input soc_sensor | |
start_hyst: !input hysteresis_start_pct | |
stop_hyst: !input hysteresis_stop_pct | |
cooldown_min: !input hold_cooldown_min | |
hyst_on: !input enable_hysteresis | |
action: | |
- alias: Charging / Exporting block | |
choose: | |
# --------------- CHARGING --------------- | |
- conditions: | |
- condition: state | |
entity_id: predbat.status | |
state: 'Charging' | |
- condition: state | |
entity_id: binary_sensor.predbat_charging | |
state: 'on' | |
sequence: | |
# ---- STOP hysteresis (only when hyst_on) ---- | |
- choose: | |
- conditions: | |
- condition: template | |
alias: "Stop hysteresis guard" | |
value_template: >- | |
{% set v1 = states('predbat.best_charge_limit')|int(0) %} | |
{% set v2 = states('predbat.charge_limit')|int(0) %} | |
{% set target = [v1, v2]|max %} | |
{% set target = 12 if target < 12 else target %} | |
{% set soc = states(soc_sensor_e)|float(0) %} | |
{{ hyst_on and (soc >= (target - stop_hyst)) }} | |
sequence: | |
- service: huawei_solar.stop_forcible_charge | |
data: { device_id: !input battery_device_id } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: 0 } | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: 0 } | |
# ---- Cooldown (skipped if hyst_on = false) ---- | |
- condition: template | |
alias: "Respect cooldown after Hold/Freeze/Demand" | |
value_template: >- | |
{% set s = states.get('predbat.status') %} | |
{% set lc = s.last_changed if s else now() %} | |
{% set secs = (now() - lc).total_seconds() %} | |
{{ (not hyst_on) or (secs >= (cooldown_min * 60)) }} | |
# ---- DAY ---- | |
- alias: Day window | |
if: | |
- condition: time | |
after: !input day_start | |
before: !input day_end | |
then: | |
- choose: | |
- conditions: | |
- condition: state | |
entity_id: binary_sensor.predbat_car_charging_slot | |
state: 'on' | |
- condition: template | |
alias: "Start hysteresis (day, car ON)" | |
value_template: >- | |
{% set v1 = states('predbat.best_charge_limit')|int(0) %} | |
{% set v2 = states('predbat.charge_limit')|int(0) %} | |
{% set target = [v1, v2]|max %} | |
{% set target = 12 if target < 12 else target %} | |
{% set soc = states(soc_sensor_e)|float(0) %} | |
{{ (not hyst_on) or (soc <= (target - start_hyst)) }} | |
sequence: | |
- service: huawei_solar.forcible_charge_soc | |
data: | |
target_soc: >- | |
{% set v1 = states('predbat.best_charge_limit')|int(0) %} | |
{% set v2 = states('predbat.charge_limit')|int(0) %} | |
{% set v = v1 if v1>0 else v2 %} | |
{{ 12 if v < 12 else v }} | |
power: !input day_car_charge_power | |
device_id: !input battery_device_id | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: !input day_car_charge_power } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: 0 } | |
default: | |
- choose: | |
- conditions: | |
- condition: template | |
alias: "Start hysteresis (day, car OFF)" | |
value_template: >- | |
{% set v1 = states('predbat.best_charge_limit')|int(0) %} | |
{% set v2 = states('predbat.charge_limit')|int(0) %} | |
{% set target = [v1, v2]|max %} | |
{% set target = 12 if target < 12 else target %} | |
{% set soc = states(soc_sensor_e)|float(0) %} | |
{{ (not hyst_on) or (soc <= (target - start_hyst)) }} | |
sequence: | |
- service: huawei_solar.forcible_charge_soc | |
data: | |
target_soc: >- | |
{% set v1 = states('predbat.best_charge_limit')|int(0) %} | |
{% set v2 = states('predbat.charge_limit')|int(0) %} | |
{% set v = v1 if v1>0 else v2 %} | |
{{ 12 if v < 12 else v }} | |
power: !input day_charge_power | |
device_id: !input battery_device_id | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: !input day_charge_power } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: 0 } | |
default: | |
- service: huawei_solar.stop_forcible_charge | |
data: { device_id: !input battery_device_id } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: 0 } | |
# ---- NIGHT ---- | |
- alias: Night window | |
if: | |
- condition: or | |
conditions: | |
- condition: time | |
before: !input day_start | |
- condition: time | |
after: !input day_end | |
then: | |
- choose: | |
- conditions: | |
- condition: state | |
entity_id: binary_sensor.predbat_car_charging_slot | |
state: 'on' | |
- condition: template | |
alias: "Start hysteresis (night, car ON)" | |
value_template: >- | |
{% set v1 = states('predbat.best_charge_limit')|int(0) %} | |
{% set v2 = states('predbat.charge_limit')|int(0) %} | |
{% set target = [v1, v2]|max %} | |
{% set target = 12 if target < 12 else target %} | |
{% set soc = states(soc_sensor_e)|float(0) %} | |
{{ (not hyst_on) or (soc <= (target - start_hyst)) }} | |
sequence: | |
- service: huawei_solar.forcible_charge_soc | |
data: | |
target_soc: >- | |
{% set v1 = states('predbat.best_charge_limit')|int(0) %} | |
{% set v2 = states('predbat.charge_limit')|int(0) %} | |
{% set v = v1 if v1>0 else v2 %} | |
{{ 12 if v < 12 else v }} | |
power: !input night_car_charge_power | |
device_id: !input battery_device_id | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: !input night_car_charge_power } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: 0 } | |
default: | |
- choose: | |
- conditions: | |
- condition: template | |
alias: "Start hysteresis (night, car OFF)" | |
value_template: >- | |
{% set v1 = states('predbat.best_charge_limit')|int(0) %} | |
{% set v2 = states('predbat.charge_limit')|int(0) %} | |
{% set target = [v1, v2]|max %} | |
{% set target = 12 if target < 12 else target %} | |
{% set soc = states(soc_sensor_e)|float(0) %} | |
{{ (not hyst_on) or (soc <= (target - start_hyst)) }} | |
sequence: | |
- service: huawei_solar.forcible_charge_soc | |
data: | |
target_soc: >- | |
{% set v1 = states('predbat.best_charge_limit')|int(0) %} | |
{% set v2 = states('predbat.charge_limit')|int(0) %} | |
{% set v = v1 if v1>0 else v2 %} | |
{{ 12 if v < 12 else v }} | |
power: !input night_charge_power | |
device_id: !input battery_device_id | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: !input night_charge_power } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: 0 } | |
default: | |
- service: huawei_solar.stop_forcible_charge | |
data: { device_id: !input battery_device_id } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: 0 } | |
# --------------- EXPORT / DISCHARGING --------------- | |
- conditions: | |
- condition: state | |
entity_id: binary_sensor.predbat_charging | |
state: 'off' | |
- condition: state | |
entity_id: binary_sensor.predbat_exporting | |
state: 'on' | |
sequence: | |
- choose: | |
# SoC >= 12%: target-SoC bounded export | |
- conditions: | |
- condition: template | |
value_template: "{{ states(soc_sensor_e) | float(0) >= 12 }}" | |
- condition: state | |
entity_id: predbat.status | |
state: 'Exporting' | |
sequence: | |
- service: huawei_solar.forcible_discharge_soc | |
data: | |
target_soc: >- | |
{% set v = states('predbat.best_export_limit') | int(0) %} | |
{{ 12 if v < 12 else v }} | |
power: !input discharge_power | |
device_id: !input battery_device_id | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: !input discharge_power } | |
# SoC < 12%: timed discharge (use non-_soc service with duration) | |
- conditions: | |
- condition: template | |
value_template: "{{ states(soc_sensor_e) | float(0) < 12 }}" | |
- condition: state | |
entity_id: predbat.status | |
state: 'Exporting' | |
sequence: | |
- service: huawei_solar.forcible_discharge | |
data: | |
duration: 120 | |
power: !input discharge_power | |
device_id: !input battery_device_id | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: !input discharge_power } | |
- alias: STOP / HOLD / FREEZE / DEMAND | |
choose: | |
- conditions: | |
- condition: state | |
entity_id: predbat.status | |
state: 'Hold for Car' | |
sequence: | |
- service: huawei_solar.stop_forcible_charge | |
data: { device_id: !input battery_device_id } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: 0 } | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: 0 } | |
- conditions: | |
- condition: or | |
conditions: | |
- condition: state | |
entity_id: predbat.status | |
state: 'Hold exporting' | |
- condition: state | |
entity_id: predbat.status | |
state: 'Freeze exporting' | |
sequence: | |
- service: huawei_solar.stop_forcible_charge | |
data: { device_id: !input battery_device_id } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: !input discharge_power } | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: !input discharge_power } | |
- conditions: | |
- condition: or | |
conditions: | |
- condition: state | |
entity_id: predbat.status | |
state: 'Hold charging' | |
- condition: state | |
entity_id: predbat.status | |
state: 'Freeze charging' | |
sequence: | |
- service: huawei_solar.stop_forcible_charge | |
data: { device_id: !input battery_device_id } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: 0 } | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: 0 } | |
- conditions: | |
- condition: state | |
entity_id: predbat.status | |
state: 'Demand' | |
sequence: | |
- service: huawei_solar.stop_forcible_charge | |
data: { device_id: !input battery_device_id } | |
- delay: '00:00:02' | |
- service: number.set_value | |
entity_id: !input number_discharging_power | |
data: { value: !input discharge_power } | |
- service: number.set_value | |
entity_id: !input number_charging_power | |
data: { value: !input discharge_power } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment