Last active
February 19, 2026 18:56
-
-
Save SunEnergyXT/fdb0cc507f7988dac558b9b83fb9259b to your computer and use it in GitHub Desktop.
deye-zero-feed-in
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: | |
| 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