Zendure.yaml inside the packages folder:
################################################################################
# ZENDURE SOLARFLOW CONTROL PACKAGE #
################################################################################
#
# DESCRIPTION:
# Advanced local control for Zendure SolarFlow with Shelly 3EM.
# Features strict validation, safe math, and separate state/loop logic.
#
# REQUIREMENTS:
# - Zendure Hyper 2000 / SolarFlow 800 Pro (HTTP API enabled)
# - Shelly Pro 3EM (Grid measurement)
# - Integration "Forecast.Solar"
#
# STRATEGY (Managed by Logic Manager):
# - WINTER: Stop at 20% SoC | Bias 10W (Import) | Saturation 250W
# - SUMMER: Stop at 8% SoC | Bias 0W (Zero) | Saturation 250W
#
# NETWORK STABILITY:
# - Polling: 15s | Timeout: 30s | No forced updates (Anti-DDoS).
#
#
# COMPLETE CHANGELOG:
# V1.0 Initial release. Basic HTTP API connection.
# V2.0 Added basic zero-export logic based on Shelly 3EM grid sum.
# V3.0 Implemented "Failsafe Mode": Fallback output if Shelly data is missing.
# V4.0 Switch to "Package" format & added Dashboard Helpers (Input Numbers).
# V4.1 Added "Emergency Charge" (AC Charge) logic for critical battery levels.
# V4.2 Introduced Seasonal Logic (Winter 20% / Summer 10% MinSoC).
# V4.3 Added "Schonmodus" (Bypass): 90% Solar pass-through between MinSoC and 30%.
# V5.0 Performance Fix: Moved monitoring to separate automation to reduce spam.
# V6.0 Added "Option 1" (Weather Forecast): Smart Emergency Charge.
# Added "Option 2" (Calibration): Weekly top-balancing logic.
# Added "Option 3" (Winter Protection): Standby if Temp < 0°C.
# V6.1 "Smart Calibration": Only force charge if not full for 7 days.
# V6.2 "Saturation Mode": Force min. 150W output if Battery > 98% and Solar > 150W.
# V6.3 High-Speed Update: Loop interval reduced to 5s. Added Delta-Check logic.
# V6.4 Precision Update: Hysteresis configurable via Slider.
# V6.5 BUGFIX: Temp Calculation (Deci-Kelvin to Celsius). Fixed Winter Protection.
# V7.0 MAJOR ARCHITECTURE UPDATE: Split Logic (Loop vs State), Safe Templates (.get),
# Command De-duplication (Last Sent Memory), Grid Bias & Fast Clamp.
# V8.0 CRITICAL FIXES & POLISH:
# - Corrected Grid Bias Math (Subtraction instead of Addition).
# - Fixed Input Number Range (Allow -2 for State Machine).
# - Security: Failsafe now strictly checks for valid SN before acting.
# - Stability: Control Loop now bases math on 'last_sent' to eliminate lag.
# - Fast Clamp: Improved trigger condition for immediate export prevention.
# - Resync: Auto-detects reboot/drift to reset internal memory.
# V9.0: CRITICAL BUGFIXES & FINAL POLISH:
# - Jinja Comment Fix: Replaced '#' with '{# #}' to prevent calculation errors.
# - Safety Gate: Execution block now explicitly demands valid SN (sn_ok).
# - Robustness: All payloads strictly cast to |int.
# - Logic: Resync mechanism automatically handles Reboot/API-Failures
# (replacing the need for complex HTTP status checks).
# V10.0: POLISHING & HARDENING:
# - Self-Healing Memory: Automatically commits "Resync" values to memory
# if drift/reboot is detected.
# - Smart Refresh: Forces an immediate sensor update after writing to the API
# to minimize the "stale data" window.
# - Logic Optimization: Cleaned up action sequences for maximum determinism.
# V11.0: PRODUCTION HARDENING:
# - Negative Guard: base_value calculation now explicitly handles negative
# memory codes (like -2) to prevent math errors after restarts.
# - Traffic Optimization: Reduced Sensor Polling to 10s (relying on
# smart memory for speed and on-demand refresh for updates).
# - Logic Cleanup: merged drift and negative checks for efficiency.
# V11.1: FINAL SAFETY UPGRADE:
# - Explicit Action Gating: Execution block now requires 'zendure_ok'
# AND 'sn_ok' to fire. This prevents writes even if calculation logic
# is accidentally modified in the future (Defense in Depth).
# V11.2: Dashboard Fix (Sanitization).
# V11.3: DATA PURITY FIX:
# - Problem: API reports positive values for BOTH Charge and Discharge
# sensors simultaneously (noise/measurement error). This confuses the
# Energy Dashboard (counting charge as discharge).
# - Fix: Implemented "Net Flow Logic". The script calculates the net
# difference (Input - Output).
# If Net > 0 -> Report Charging, force Discharging to 0.
# If Net < 0 -> Report Discharging, force Charging to 0.
# This makes the sensors mutually exclusive, as per physics.
# V11.4: PERFECT MERGE:
# - Combines V11.2 and V11.3 logic.
# - Raw sensor data is FIRST sanitized (clipping negatives to 0)
# - THEN the net flow is calculated.
# - Prevents "Ghost Charging" caused by negative noise on the output channel.
# V11.5: SENSOR SWAP FIX:
# - Diagnosis: User reported that Charging Energy appears as Discharging
# in Home Assistant, despite correct Dashboard config.
# - Fix: SWAPPED the API mapping in the Template Sensors.
# "Pack Input Power" (Charge) now reads 'outputPackPower' from API.
# "Pack Output Power" (Discharge) now reads 'packInputPower' from API.
# This aligns the script with the device's actual reporting behavior.
# V11.6: STRATEGY UPDATE:
# - Summer Soft-Stop set to 8% (Logic Manager).
# - Emergency Charge Start set to 6% (Buffer before Hardware Limit).
# - Hardware Limit remains 5% (in Zendure App).
# - Emergency Charge Stop remains 15% (Healthy Hysteresis).
# V11.7: STABILITY UPDATE:
# - Removed version numbers from Automation Aliases and IDs.
# - Ensures that recorder/logbook exclusions in configuration.yaml
# remain valid even after future script updates.
# V11.8: NETWORK STABILITY FIX:
# - Diagnosis: User reported periodic "drops to 0" on all sensors.
# Cause: The aggressive "Write + Immediate Read" logic overwhelmed the
# ESP32 chip of the Zendure, causing connection drops.
# - Fix 1: Removed 'homeassistant.update_entity' (Force Refresh).
# We now rely purely on internal memory for fast control, giving the
# device time to breathe.
# - Fix 2: Increased Polling Interval to 15s.
# - Fix 3: Updated Template Sensors to return 'unavailable' instead of '0'
# when API data is missing. This prevents "sawtooth" graphs.
################################################################################
# ==============================================================================
# 1. KONFIGURATION (HELFER)
# ==============================================================================
input_boolean:
zendure_auto_mode:
name: "Zendure Automatik Aktiv"
icon: mdi:robot
initial: true
zendure_emergency_charge:
name: "Status: Not-Ladung läuft"
icon: mdi:battery-alert
initial: false
zendure_calibration_mode:
name: "Status: Kalibrierung läuft"
icon: mdi:battery-sync
initial: false
input_datetime:
zendure_last_full_charge:
name: "Letzte 100% Ladung am"
has_date: true
has_time: true
input_number:
# --- INTERNE SPEICHER ---
zendure_last_sent_limit:
name: "System: Letzter gesendeter Wert"
min: -2
max: 1200
step: 1
mode: box
# --- EINSTELLUNGEN ---
zendure_conf_max_output:
name: "Zendure: Max. Einspeisung"
min: 0
max: 800
step: 10
initial: 800
unit_of_measurement: "W"
mode: box
zendure_conf_failsafe_watt:
name: "Zendure: Failsafe Grundlast"
icon: mdi:network-off-outline
min: 0
max: 800
step: 10
initial: 130
unit_of_measurement: "W"
mode: box
zendure_conf_saturation_watt:
name: "Zendure: Min. Einspeisung bei 100%"
icon: mdi:water-percent
min: 0
max: 800
step: 10
initial: 250
unit_of_measurement: "W"
mode: box
# Grid Bias: Ziel ist ein leichter Netzbezug (Import).
# Allow negative values for Summer Export Strategy
zendure_conf_grid_bias:
name: "Zendure: Netz-Puffer (Bias)"
icon: mdi:arrow-collapse-right
min: -100
max: 100
step: 5
initial: 10
unit_of_measurement: "W"
mode: box
zendure_conf_hysteresis:
name: "Zendure: Regel-Hysterese"
icon: mdi:target-variant
min: 1
max: 50
step: 1
initial: 5
unit_of_measurement: "W"
mode: box
zendure_conf_emergency_watt:
name: "Zendure: AC-Ladegeschwindigkeit"
icon: mdi:flash
min: 50
max: 1200
step: 50
initial: 500
unit_of_measurement: "W"
mode: box
# --- BATTERIE GRENZEN ---
zendure_conf_soc_off:
name: "SoC Limit: Not-Aus (Auto-Saison)"
icon: mdi:battery-off
min: 0
max: 30
initial: 20
unit_of_measurement: "%"
zendure_conf_soc_bypass:
name: "SoC Limit: Schonmodus"
icon: mdi:battery-heart-variant
min: 10
max: 90
initial: 30
unit_of_measurement: "%"
# Start Not-Ladung
zendure_conf_soc_emerg_start:
name: "SoC Limit: Start Not-Ladung"
min: 0
max: 20
initial: 6
unit_of_measurement: "%"
# Stopp Not-Ladung
zendure_conf_soc_emerg_stop:
name: "SoC Limit: Stopp Not-Ladung"
min: 5
max: 50
initial: 15
unit_of_measurement: "%"
# --- LOGIK OPTIONEN ---
zendure_conf_forecast_thresh:
name: "Wetter: kWh morgen für Lade-Stopp"
icon: mdi:weather-sunny
min: 0
max: 20
step: 0.5
initial: 2.5
unit_of_measurement: "kWh"
mode: box
zendure_conf_calib_days:
name: "Kalibrierung: Max Tage ohne 100%"
icon: mdi:calendar-refresh
min: 1
max: 30
step: 1
initial: 7
unit_of_measurement: "Tage"
mode: box
# ==============================================================================
# 2. SENSOREN (SAFE MODE & RELAXED POLLING)
# ==============================================================================
sensor:
- platform: rest
name: "SolarFlow Status Raw"
resource: "http://192.168.30.91/properties/report"
method: GET
# V11.9: Interval 15s, Timeout 30s (Network Stability)
scan_interval: 15
timeout: 30
value_template: "OK"
json_attributes:
- sn
- properties
- platform: integration
source: sensor.solarflow_solar_input
name: "SolarFlow Solar Ertrag Gesamt"
unit_prefix: k
round: 3
method: left
- platform: integration
source: sensor.solarflow_pack_input_power
name: "SolarFlow Batterie Energie Geladen"
unit_prefix: k
round: 3
method: left
- platform: integration
source: sensor.solarflow_pack_output_power
name: "SolarFlow Batterie Energie Entladen"
unit_prefix: k
round: 3
method: left
# ==============================================================================
# 3. TEMPLATE SENSOREN (ZERO-DROP PROTECTION)
# ==============================================================================
template:
- sensor:
- name: "SolarFlow Battery Level"
unique_id: solarflow_battery_level
unit_of_measurement: "%"
device_class: battery
state: >
{% set p = state_attr('sensor.solarflow_status_raw','properties') %}
{% if p is mapping %}
{{ p.get('electricLevel', 0) | int }}
{% else %}
unavailable
{% endif %}
- name: "SolarFlow Output Limit"
unique_id: solarflow_output_limit
unit_of_measurement: "W"
device_class: power
state: >
{% set p = state_attr('sensor.solarflow_status_raw','properties') %}
{% if p is mapping %}
{{ p.get('outputLimit', 0) | int }}
{% else %}
unavailable
{% endif %}
- name: "SolarFlow Solar Input"
unique_id: solarflow_solar_input
unit_of_measurement: "W"
device_class: power
state: >
{% set p = state_attr('sensor.solarflow_status_raw','properties') %}
{% if p is mapping %}
{% set val = p.get('solarInputPower', 0) | int %}
{{ [val, 0] | max }}
{% else %}
unavailable
{% endif %}
- name: "SolarFlow Device Temp"
unique_id: solarflow_device_temp
unit_of_measurement: "°C"
device_class: temperature
state: >-
{% set p = state_attr('sensor.solarflow_status_raw','properties') %}
{% if p is mapping %}
{% set raw = p.get('hyperTmp', 2931) | int %}
{{ ((raw / 10) - 273.15) | round(1) }}
{% else %}
unavailable
{% endif %}
# NET FLOW LOGIC (Prevents Dashboard Errors) & SENSOR SWAP
- name: "SolarFlow Pack Input Power"
unique_id: solarflow_pack_input_power
unit_of_measurement: "W"
device_class: power
state: >
{% set p = state_attr('sensor.solarflow_status_raw','properties') %}
{% if p is mapping %}
{# SWAP: Charge Sensor reads 'outputPackPower' #}
{% set in_raw = [p.get('outputPackPower', 0)|int, 0] | max %}
{# SWAP: Discharge Sensor reads 'packInputPower' #}
{% set out_raw = [p.get('packInputPower', 0)|int, 0] | max %}
{% set net = in_raw - out_raw %}
{% if net > 0 %} {{ net }} {% else %} 0 {% endif %}
{% else %}
unavailable
{% endif %}
- name: "SolarFlow Pack Output Power"
unique_id: solarflow_pack_output_power
unit_of_measurement: "W"
device_class: power
state: >
{% set p = state_attr('sensor.solarflow_status_raw','properties') %}
{% if p is mapping %}
{# SWAP: Charge Sensor reads 'outputPackPower' #}
{% set in_raw = [p.get('outputPackPower', 0)|int, 0] | max %}
{# SWAP: Discharge Sensor reads 'packInputPower' #}
{% set out_raw = [p.get('packInputPower', 0)|int, 0] | max %}
{% set net = in_raw - out_raw %}
{% if net < 0 %} {{ net | abs }} {% else %} 0 {% endif %}
{% else %}
unavailable
{% endif %}
# ==============================================================================
# 4. REST COMMANDS (ROBUST)
# ==============================================================================
rest_command:
zendure_set_output:
url: "http://192.168.30.91/properties/write"
method: POST
headers:
Content-Type: application/json
payload: >
{
"properties": {
"outputLimit": {{ limit | int }},
"chargeMaxLimit": 0
},
"sn": "{{ state_attr('sensor.solarflow_status_raw', 'sn') }}"
}
zendure_force_charge:
url: "http://192.168.30.91/properties/write"
method: POST
headers:
Content-Type: application/json
payload: >
{
"properties": {
"outputLimit": 0,
"chargeMaxLimit": {{ limit | int }}
},
"sn": "{{ state_attr('sensor.solarflow_status_raw', 'sn') }}"
}
# ==============================================================================
# 5. AUTOMATISIERUNG A: DIE STATUS-MASCHINE
# ==============================================================================
automation:
- alias: "Zendure: State Machine"
id: "zendure_state_machine_stable"
mode: restart
trigger:
- platform: state
entity_id:
- input_boolean.zendure_emergency_charge
- input_boolean.zendure_calibration_mode
condition:
# Safety: SN muss existieren
- condition: template
value_template: "{{ state_attr('sensor.solarflow_status_raw','sn') is not none }}"
action:
- choose:
# FALL 1: START LADUNG
- conditions: >
{{ is_state('input_boolean.zendure_emergency_charge','on')
or is_state('input_boolean.zendure_calibration_mode','on') }}
sequence:
- service: rest_command.zendure_force_charge
data:
limit: "{{ states('input_number.zendure_conf_emergency_watt')|int }}"
- service: input_number.set_value
target:
entity_id: input_number.zendure_last_sent_limit
data:
value: -2
# FALL 2: STOP LADUNG (Zurück zu 0W)
- conditions: >
{{ is_state('input_boolean.zendure_emergency_charge','off')
and is_state('input_boolean.zendure_calibration_mode','off') }}
sequence:
- service: rest_command.zendure_set_output
data:
limit: 0
- service: input_number.set_value
target:
entity_id: input_number.zendure_last_sent_limit
data:
value: 0
# ==============================================================================
# 6. AUTOMATISIERUNG B: DER REGEL-LOOP (MAIN LOOP)
# ==============================================================================
- alias: "Zendure: Control Loop"
id: "zendure_control_loop_stable"
mode: restart
trigger:
- platform: time_pattern
seconds: "/5"
condition:
- condition: state
entity_id: input_boolean.zendure_auto_mode
state: "on"
# Pause während Lademodus
- condition: state
entity_id: input_boolean.zendure_emergency_charge
state: "off"
- condition: state
entity_id: input_boolean.zendure_calibration_mode
state: "off"
action:
- variables:
# --- CONFIG ---
max_output: "{{ states('input_number.zendure_conf_max_output') | int }}"
failsafe_watt: "{{ states('input_number.zendure_conf_failsafe_watt') | int }}"
saturation_watt: "{{ states('input_number.zendure_conf_saturation_watt') | int }}"
hysteresis_watt: "{{ states('input_number.zendure_conf_hysteresis') | int }}"
grid_bias: "{{ states('input_number.zendure_conf_grid_bias') | int }}"
soc_off: "{{ states('input_number.zendure_conf_soc_off') | int }}"
soc_bypass: "{{ states('input_number.zendure_conf_soc_bypass') | int }}"
# --- SENSORS & MEMORY ---
shelly_ok: "{{ states('sensor.power_import') | is_number and states('sensor.power_export') | is_number }}"
zendure_ok: "{{ states('sensor.solarflow_status_raw') == 'OK' }}"
sn_ok: "{{ state_attr('sensor.solarflow_status_raw','sn') is not none }}"
# Memory Logic
raw_last_sent: "{{ states('input_number.zendure_last_sent_limit') | int }}"
current_limit: "{{ states('sensor.solarflow_output_limit') | float(0) }}"
# Check Drift
is_drifting: "{{ raw_last_sent >= 0 and (raw_last_sent - current_limit)|abs > 150 }}"
# Safe Base
base_value: >
{% if raw_last_sent < 0 or is_drifting %}
{{ current_limit }}
{% else %}
{{ raw_last_sent }}
{% endif %}
temp_device: "{{ states('sensor.solarflow_device_temp') | float(20) }}"
battery: "{{ states('sensor.solarflow_battery_level') | float(0) }}"
pv_power: "{{ states('sensor.solarflow_solar_input') | float(0) }}"
grid_power: >
{% if shelly_ok %}
{{ (states('sensor.power_import') | float(0) - states('sensor.power_export') | float(0)) }}
{% else %} 0 {% endif %}
# --- SELF-HEALING MEMORY ---
- choose:
- conditions: "{{ is_drifting }}"
sequence:
- service: input_number.set_value
target:
entity_id: input_number.zendure_last_sent_limit
data:
value: "{{ current_limit | int }}"
# --- CALCULATION BLOCK ---
- variables:
calc_target: >
{# PRIO 1: FAILSAFE (Strict SN Check) #}
{% if not shelly_ok and zendure_ok and sn_ok %}
{{ failsafe_watt }}
{# PRIO 2: API/SN ERROR #}
{% elif not (zendure_ok and sn_ok) %}
-1
{# PRIO 3: WINTER PROTECTION #}
{% elif temp_device < 0 and pv_power < 100 %}
0
{# PRIO 4: BATTERY EMPTY #}
{% elif battery <= soc_off %}
0
{# PRIO 5: BYPASS (SCHONMODUS) #}
{% elif battery > soc_off and battery <= soc_bypass %}
{# Math: Target = House Load - Bias. #}
{% set house_demand = base_value + grid_power - grid_bias %}
{% set available_solar = (pv_power * 0.9) | int %}
{% set tmp = [house_demand, available_solar] | min %}
{% if tmp < 0 %} 0 {% else %} {{ tmp | int }} {% endif %}
{# PRIO 6: NORMAL OPERATION #}
{% else %}
{# Base logic: Start from Last Sent (Base), add Grid discrepancy, subtract Bias. #}
{% set target_base = base_value + grid_power - grid_bias %}
{# Saturation Logic #}
{% if battery > 98 and pv_power > saturation_watt %}
{% set target_final = [target_base, saturation_watt] | max %}
{% else %}
{% set target_final = target_base %}
{% endif %}
{# Safety Bounds #}
{% if target_final < 0 %} 0
{% elif target_final > max_output %} {{ max_output }}
{% else %} {{ target_final | int }} {% endif %}
{% endif %}
# --- EXECUTION BLOCK (GATED) ---
- choose:
# Case: Error -> Abort
- conditions: "{{ calc_target | int == -1 }}"
sequence: []
# Case: Fast Export Clamp
- conditions:
- condition: template
value_template: "{{ zendure_ok and sn_ok and grid_power < -80 and calc_target < base_value }}"
sequence:
- service: rest_command.zendure_set_output
data:
limit: "{{ calc_target | int }}"
- service: input_number.set_value
target:
entity_id: input_number.zendure_last_sent_limit
data:
value: "{{ calc_target | int }}"
# Case: Normal Adjustment
- conditions:
- condition: template
value_template: "{{ zendure_ok and sn_ok }}"
- condition: template
value_template: "{{ (calc_target | int - base_value) | abs > hysteresis_watt }}"
sequence:
- service: rest_command.zendure_set_output
data:
limit: "{{ calc_target | int }}"
- service: input_number.set_value
target:
entity_id: input_number.zendure_last_sent_limit
data:
value: "{{ calc_target | int }}"
# ==============================================================================
# 7. AUTOMATISIERUNG C: MANAGER (DAILY & CHECK)
# ==============================================================================
# Track last time battery was full
- alias: "Zendure: Tracker 100% Ladung"
id: "zendure_track_100_percent_stable"
trigger:
- platform: numeric_state
entity_id: sensor.solarflow_battery_level
above: 99
action:
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.zendure_last_full_charge
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
# Logic Manager
- alias: "Zendure: Logic Manager"
id: "zendure_logic_manager_stable"
trigger:
- platform: time
at: "00:00:00"
- platform: time_pattern
minutes: "/10"
- platform: state
entity_id: sensor.solarflow_battery_level
action:
# 1. Season Switch (SoC Limit & Grid Bias & Saturation)
- choose:
# --- WINTER (Oktober bis März) ---
- conditions: "{{ now().month >= 10 or now().month < 4 }}"
sequence:
# Limit: Früher aufhören (20%)
- service: input_number.set_value
target:
entity_id: input_number.zendure_conf_soc_off
data:
value: 20
# Bias: 10W Netzbezug zulassen (Sicherheit)
- service: input_number.set_value
target:
entity_id: input_number.zendure_conf_grid_bias
data:
value: 10
# Sättigung: 250W (ANGEPASST auf deinen Wunsch)
- service: input_number.set_value
target:
entity_id: input_number.zendure_conf_saturation_watt
data:
value: 250 # <--- HIER GEÄNDERT VON 150 AUF 250
# --- SOMMER (April bis September) ---
- conditions: "{{ now().month >= 4 and now().month < 10 }}"
sequence:
# Limit: Tief entladen (8%)
- service: input_number.set_value
target:
entity_id: input_number.zendure_conf_soc_off
data:
value: 8
# Bias: 0W (Echte Nulleinspeisung)
- service: input_number.set_value
target:
entity_id: input_number.zendure_conf_grid_bias
data:
value: 0
# Sättigung: 250W (Deine Wunsch-Einstellung)
- service: input_number.set_value
target:
entity_id: input_number.zendure_conf_saturation_watt
data:
value: 250
# 2. Check Emergency Charge
- variables:
bat: "{{ states('sensor.solarflow_battery_level')|float(0) }}"
start_lim: "{{ states('input_number.zendure_conf_soc_emerg_start')|int }}"
stop_lim: "{{ states('input_number.zendure_conf_soc_emerg_stop')|int }}"
forecast: "{{ states('sensor.energy_production_tomorrow')|float(0) }}"
thresh: "{{ states('input_number.zendure_conf_forecast_thresh')|float }}"
- choose:
- conditions:
- condition: template
value_template: "{{ bat < start_lim }}"
- condition: template
value_template: "{{ forecast < thresh or bat < 3 }}"
- condition: state
entity_id: input_boolean.zendure_emergency_charge
state: "off"
sequence:
- service: input_boolean.turn_on
target:
entity_id: input_boolean.zendure_emergency_charge
- conditions:
- condition: template
value_template: "{{ bat > stop_lim }}"
- condition: state
entity_id: input_boolean.zendure_emergency_charge
state: "on"
sequence:
- service: input_boolean.turn_off
target:
entity_id: input_boolean.zendure_emergency_charge
# 3. Check Calibration
- variables:
days_threshold: "{{ states('input_number.zendure_conf_calib_days') | int }}"
last_run: "{{ states('input_datetime.zendure_last_full_charge') }}"
days_diff: >
{% if last_run not in ['unknown', 'unavailable'] %}
{{ (as_timestamp(now()) - as_timestamp(last_run)) / 86400 }}
{% else %} 999 {% endif %}
- choose:
- conditions:
- condition: template
value_template: "{{ days_diff | float >= days_threshold }}"
- condition: state
entity_id: input_boolean.zendure_calibration_mode
state: "off"
sequence:
- service: input_boolean.turn_on
target:
entity_id: input_boolean.zendure_calibration_mode
- service: persistent_notification.create
data:
title: "Zendure Zellpflege"
message: "Start Kalibrierung ({{ days_diff | int }} Tage nicht voll)."
- conditions:
- condition: template
value_template: "{{ bat >= 100 }}"
- condition: state
entity_id: input_boolean.zendure_calibration_mode
state: "on"
sequence:
- service: input_boolean.turn_off
target:
entity_id: input_boolean.zendure_calibration_mode
- service: persistent_notification.create
data:
title: "Zendure Kalibrierung"
message: "Beendet (100% erreicht)."
# ==============================================================================
# 8. MONITORING
# ==============================================================================
- alias: "Zendure: Warnung bei Shelly Ausfall"
id: "zendure_notify_shelly_fail_stable"
trigger:
- platform: template
value_template: >
{{ not (states('sensor.power_import') | is_number and states('sensor.power_export') | is_number) }}
for: "00:00:30"
action:
- service: persistent_notification.create
data:
title: "⚠️ Zendure Failsafe"
message: "Shelly offline. Grundlast aktiv."
notification_id: "zendure_failsafe_alert"
- alias: "Zendure: Entwarnung bei Shelly Recovery"
id: "zendure_notify_shelly_ok_stable"
trigger:
- platform: template
value_template: >
{{ states('sensor.power_import') | is_number and states('sensor.power_export') | is_number }}
for: "00:00:10"
action:
- service: persistent_notification.dismiss
data:
notification_id: "zendure_failsafe_alert"