Skip to content

Instantly share code, notes, and snippets.

@JohanAlvedal
Last active August 26, 2025 04:23
Show Gist options
  • Save JohanAlvedal/56e06d6b7b62cf4a080bb04d0e5b015f to your computer and use it in GitHub Desktop.
Save JohanAlvedal/56e06d6b7b62cf4a080bb04d0e5b015f to your computer and use it in GitHub Desktop.
Predbat Charging & Discharging Control (Huawei Solar)
# =============================
# 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