Skip to content

Instantly share code, notes, and snippets.

@SunEnergyXT
Last active February 19, 2026 18:56
Show Gist options
  • Select an option

  • Save SunEnergyXT/fdb0cc507f7988dac558b9b83fb9259b to your computer and use it in GitHub Desktop.

Select an option

Save SunEnergyXT/fdb0cc507f7988dac558b9b83fb9259b to your computer and use it in GitHub Desktop.
deye-zero-feed-in
blueprint:
name: Deye Zero Feed-in
description: |
Advanced PI Zero Feed-in Control designed for 1-2 Deye Microinverters.
Key Features:
1. Event-Driven: Triggered strictly by Shelly power sensor state changes.
2. Auto-Shutdown Timeout Management: Dynamically adjusts the battery system's auto-shutdown timeout duration based on whether the remaining battery SoC exceeds 10%.
3. Discharge Protection Control: Regulates microinverter output based on a dynamic discharge floor (Maximum of two limits + 3% buffer) to prevent over-discharge.
4. PI Control: Utilizes an incremental PI algorithm to eliminate steady-state error, ensuring precise zero feed-in and smooth power regulation without oscillation.
domain: automation
input:
# --- Sensors ---
shelly_power_sensor:
name: Shelly total active power sensor (Required)
selector: { entity: { domain: sensor, device_class: power } }
avg_battery_soc:
name: Battery remaining soc (Required)
selector: { entity: { domain: sensor, device_class: battery } }
timeout_no_io_entity:
name: Battery no-input/no-output auto-shutdown timeout (Required)
selector: { entity: { domain: number } }
timeout_dod_entity:
name: Battery DOD-limit auto-shutdown timeout (Required)
selector: { entity: { domain: number } }
discharge_limit_a:
name: Minimum SOC allowed for discharge (Required)
selector: { entity: { domain: number} }
discharge_limit_b:
name: Battery BMS hardware discharge-limit SOC (Required)
selector: { entity: { domain: sensor, device_class: battery } }
integral_store_entity:
name: PI cumulative points storage entity (Required)
selector: { entity: { domain: input_number } }
inverter1_control:
name: Inverter 1 active power regulation entity (Required)
selector: { entity: { domain: number } }
inverter1_rated:
name: Inverter 1 rated power (W) (Required)
selector: { number: { min: 300, max: 2500, mode: box, unit_of_measurement: W } }
inverter2_control:
name: Inverter 2 active power regulation entity (Optional)
default: [ ]
selector: { entity: { domain: number } }
inverter2_rated:
name: Inverter 2 rated power (W) (Optional)
default: 0
selector: { number: { min: 300, max: 2500, mode: box, unit_of_measurement: W } }
# Concurrency Control: Single mode to prevent race conditions
mode: single
max_exceeded: silent
variables:
# --- Input Mapping ---
grid_sensor: !input shelly_power_sensor
avg_soc_sensor: !input avg_battery_soc
integral_entity: !input integral_store_entity
# Timeout Entities
timeout_no_io: !input timeout_no_io_entity
timeout_dod: !input timeout_dod_entity
# Discharge Limit Entities
limit_a_ent: !input discharge_limit_a
limit_b_ent: !input discharge_limit_b
# Inverter Configuration
inv1_ent: !input inverter1_control
inv1_pow: !input inverter1_rated
inv2_ent: !input inverter2_control
inv2_pow: !input inverter2_rated
# PID Parameters (Hardcoded constants)
kp: 0.7
ki: 0.05
# Timeout Management Constants (Minutes)
c_soc_threshold: 10
c_timeout_long: 1440
c_timeout_def_no_io: 15
c_timeout_def_dod: 5
# Deadband Constants (Fixed Wattage)
c_deadband_min: -30 # -30W (Exporting slightly is allowed)
c_deadband_max: 30 # 30W (Importing slightly is allowed)
# Protection Constants
c_buffer_soc: 3 # 3% Buffer
c_deadband_pct: 0.01 # 1% of Rated Power
# Timing Correction Constants
c_dt_clamp_threshold: 60 # If latency > 60s, assume system idle
c_dt_default: 15 # Default dt when clamped
# Output Regulation
c_integral_limit_pct: 0.3 # Integral anti-windup limit (30% of Rated)
c_output_hysteresis: 0.1 # Minimum change to trigger write (0.1%)
# Check if Inverter 2 is configured
inv2_exists: >
{{ inv2_ent != [] and inv2_ent != None and inv2_pow > 0 }}
# Calculate Total Rated Power
p_sum: >
{{ inv1_pow + (inv2_pow if inv2_exists else 0) }}
trigger:
# Trigger only on Shelly power sensor state change
- platform: state
entity_id: !input shelly_power_sensor
action:
# 1. Retrieve Real-time States
- variables:
avg_soc: "{{ states(avg_soc_sensor) | float(0) }}"
p_grid: "{{ states(grid_sensor) | float(0) }}"
# Determine the effective discharge limit (Max of Source A and B)
limit_a: "{{ states(limit_a_ent) | float(0) }}"
limit_b: "{{ states(limit_b_ent) | float(0) }}"
real_min_soc: "{{ [limit_a, limit_b] | max }}"
# Get current values of timeout settings
curr_timeout_no_io: "{{ states(timeout_no_io) | float(0) }}"
curr_timeout_dod: "{{ states(timeout_dod) | float(0) }}"
# 2. Automatic Timeout Management
- choose:
# Case A: Sufficient Charge (> Threshold). Extend timeout to standby mode.
# Logic: Only update if current value matches the factory default.
- conditions:
- condition: template
value_template: "{{ avg_soc > c_soc_threshold }}"
sequence:
- if:
- condition: template
value_template: "{{ curr_timeout_no_io == c_timeout_def_no_io }}"
then:
- service: number.set_value
target: { entity_id: "{{ timeout_no_io }}" }
data: { value: "{{ c_timeout_long }}" }
- if:
- condition: template
value_template: "{{ curr_timeout_dod == c_timeout_def_dod }}"
then:
- service: number.set_value
target: { entity_id: "{{ timeout_dod }}" }
data: { value: "{{ c_timeout_long }}" }
# Case B: Low Charge (<= Threshold). Revert timeout to factory defaults.
# Logic: Only update if current value is NOT the factory default.
- conditions:
- condition: template
value_template: "{{ avg_soc <= c_soc_threshold }}"
sequence:
- if:
- condition: template
value_template: "{{ curr_timeout_no_io != c_timeout_def_no_io }}"
then:
- service: number.set_value
target: { entity_id: "{{ timeout_no_io }}" }
data: { value: "{{ c_timeout_def_no_io }}" }
- if:
- condition: template
value_template: "{{ curr_timeout_dod != c_timeout_def_dod }}"
then:
- service: number.set_value
target: { entity_id: "{{ timeout_dod }}" }
data: { value: "{{ c_timeout_def_dod }}" }
# 3. Discharge Protection Check
# Condition: If Avg SoC <= (Effective Limit + Buffer), shutdown inverters.
- choose:
- conditions:
- condition: template
value_template: "{{ avg_soc <= (real_min_soc + c_buffer_soc) }}"
sequence:
# Set Inverter 1 to 0
- service: number.set_value
target: { entity_id: "{{ inv1_ent }}" }
data: { value: 0 }
# Set Inverter 2 to 0 (if exists)
- if:
- condition: template
value_template: "{{ inv2_exists }}"
then:
- service: number.set_value
target: { entity_id: "{{ inv2_ent }}" }
data: { value: 0 }
# Reset PI Integral
- service: input_number.set_value
target: { entity_id: "{{ integral_entity }}" }
data: { value: 0 }
- stop: "Low battery protection activated (SoC <= Limit+3%). Output forced to 0."
# 4. Deadband Check
# Skip adjustment if Grid Power is within -30W to 30W.
- if:
- condition: template
value_template: "{{ p_grid > c_deadband_min and p_grid < c_deadband_max }}"
then:
- stop: "Within deadband (-30W < Grid < 30W), skipping adjustment."
# 5. Core PI Calculation
- variables:
# a. Calculate Time Delta (dt)
# Get the latest 'last_changed' timestamp from inverters
t1_val: >
{{ as_timestamp(states[inv1_ent].last_changed) if states[inv1_ent].last_changed else 0 }}
t2_val: >
{{ as_timestamp(states[inv2_ent].last_changed) if inv2_exists and states[inv2_ent].last_changed else 0 }}
last_ts: "{{ t1_val if t1_val > t2_val else t2_val }}"
current_ts: "{{ now().timestamp() }}"
raw_diff: >
{% if last_ts == 0 %} 999
{% else %} {{ current_ts - last_ts }}
{% endif %}
# b. DT Correction
# If raw_diff > threshold (system idle/startup), clamp dt to default to prevent integral windup.
dt: >
{% if raw_diff > c_dt_clamp_threshold %} {{ c_dt_default }}
{% else %} {{ raw_diff | round(3) }}
{% endif %}
# c. Calculate Error (Target Grid Power = 0)
error: "{{ -p_grid }}"
# d. Retrieve Historical Data
last_integral: "{{ states(integral_entity) | float(0) }}"
last_percent: "{{ states(inv1_ent) | float(0) }}"
# e. Calculate New Integral (with anti-windup limit)
integ_limit: "{{ p_sum * c_integral_limit_pct }}"
raw_new_integ: "{{ last_integral + (ki * error * dt) }}"
new_integral: >
{% if raw_new_integ > integ_limit %} {{ integ_limit }}
{% elif raw_new_integ < -integ_limit %} {{ -integ_limit }}
{% else %} {{ raw_new_integ }}
{% endif %}
# f. Calculate Power Offset & Target Power
p_offset: "{{ (kp * error) + new_integral }}"
# Formula: Target = Last Power - Offset
raw_target_p: >
{{ ((last_percent / 100) * p_sum) - p_offset }}
target_p: >
{% if raw_target_p > p_sum %} {{ p_sum }}
{% elif raw_target_p < 0 %} 0
{% else %} {{ raw_target_p }}
{% endif %}
# g. Calculate Final Percentage
new_percent: >
{{ (target_p / p_sum * 100) | round(1) }}
# 6. Execute Output
# Only send command if the change exceeds hysteresis to reduce bus traffic
- if:
- condition: template
value_template: "{{ (new_percent - last_percent) | abs > c_output_hysteresis }}"
then:
# Update Integral Storage
- service: input_number.set_value
target: { entity_id: "{{ integral_entity }}" }
data: { value: "{{ new_integral | round(2) }}" }
# Update Inverter 1
- service: number.set_value
target: { entity_id: "{{ inv1_ent }}" }
data: { value: "{{ new_percent }}" }
# Update Inverter 2 (if exists)
- if:
- condition: template
value_template: "{{ inv2_exists }}"
then:
- service: number.set_value
target: { entity_id: "{{ inv2_ent }}" }
data: { value: "{{ new_percent }}" }
# Log PI Operation
- service: system_log.write
data:
level: info
message: >
PI: Grid={{ p_grid }}W, dt={{ dt }}s (raw={{ raw_diff | round(1) }}),
Err={{ error }}, Integ={{ new_integral | round(1) }},
Out: {{ last_percent }}% -> {{ new_percent }}%
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment