Bonjour à la communauté,
Je vous partage un script (Automation YAML) que j’ai peaufiné pour piloter la charge batterie d’un onduleur Deye (ou Sunsynk) via Home Assistant et Solar Assistant.
Le Contexte & La Contrainte (Pourquoi ce script ?) Je suis dans une configuration de rénovation (Retrofit) avec une particularité contraignante :
- J’ai déjà une installation solaire existante (non compatible batterie).
- J’ai ajouté un Deye pour gérer une batterie, mais je ne pouvais le brancher que sur le port GRID (pas de possibilité de le mettre en coupure ou d’utiliser le port GEN/Load).
- Le Problème : Branché uniquement sur le port Grid, le Deye ne sait pas gérer nativement la charge via le surplus de l’autre onduleur. Il est soit en « charge secteur » (fixe), soit rien. Il est « aveugle » à ce qui se passe sur le compteur général.
La Solution J’ai donc créé un « Cerveau » externe via Home Assistant. Le script lit mon compteur réseau (P1 Meter) et pilote dynamiquement le courant de charge du Deye (en MQTT) pour maintenir l’exportation vers le réseau proche de 0, tout en absorbant le maximum de solaire.
Les Fonctionnalités (Version 17.1 Titanium) Ce n’est pas une simple régulation « Si soleil > charge », c’est un véritable algorithme de contrôle :
Protection de la Mémoire Flash (EEPROM) :
- C’était une contrainte majeure : ne pas tuer l’onduleur en écrivant toutes les 2 secondes.
- Je me suis fixé une limite de 200 écritures max par jour.
- Grâce à une logique de filtrage adaptatif (ne pas écrire pour des variations mineures), mes tests actuels montrent entre 50 et 150 écritures/jour selon la météo, ce qui garantit une excellente durée de vie.
Courbe de Charge « Superchargeur » :
- Batterie vide : Autorise une charge très rapide (jusqu’à 120A dans mon cas) pour « boire » les pics de production.
- Batterie pleine : Réduit linéairement le courant (90A → 0A) pour équilibrer les cellules et respecter le BMS.
Mode Panic (Protection Réseau) :
- Analyse la « dérivée » (vitesse de variation) du réseau. Si j’allume le four ou qu’un nuage passe, la charge coupe instantanément pour ne pas importer du réseau.
Stabilité Adaptative :
- Distingue les moments « Calmes » (régulation fine au Watt près) des moments « Instables » (régulation plus souple).
Appel à la communauté Ce script est le résultat de mes tests et besoins, mais il n’est certainement pas parfait. Je suis ouvert à toute idée d’amélioration, optimisation ou critique constructive ! Mon but en partageant ce travail est avant tout d’aider ceux qui se retrouvent dans la même impasse technique que moi (Deye sur port Grid + besoin de gérer le surplus) et qui cherchent une solution logicielle fiable.
Pré-requis
- Home Assistant + Solar Assistant (MQTT).
- Créer 2 Entrées dans HA :
input_datetime.deye_last_major_changecounter.compteur_ecritures_deye
Adaptation indispensable Le code ci-dessous est fait pour MON installation. Il est impératif d’adapter les sensors à votre cas :
- Remplacez
sensor.p1_meter_puissancepar votre capteur de réseau global (Linky, Shelly EM, pince CT…). - Ajustez les variables
hardware_limitetboost_ampsselon la section de vos câbles ! (J’ai du câblage dimensionné pour 120A, ne faites pas ça sur du 25mm² !).
Le Code YAML :
alias: "Gestion Surplus Deye - V17.1 (Titanium Edition)"
description: "V17.1 : Version Durcie. Courbe de charge 3 phases + Sécurité Hard-Cap + Hystérésis de phase + Logs détaillés. Le summum de la gestion batterie."
mode: restart
triggers:
- entity_id: sensor.p1_meter_puissance
trigger: state
- minutes: /1
trigger: time_pattern
conditions:
- condition: state
entity_id: sun.sun
state: above_horizon
# Rythme : 15 secondes
- condition: template
value_template: >
{{ (now() - this.attributes.last_triggered | default(now() - timedelta(seconds=20), true)).total_seconds() > 14 }}
- condition: not
conditions:
- condition: state
entity_id: number.deye_sunsynk_sol_ark_3_phase_max_grid_charge_current
state: unavailable
actions:
- delay:
seconds: 1
- variables:
# --- CAPTEURS ---
charge_entity: number.deye_sunsynk_sol_ark_3_phase_max_grid_charge_current
write_counter: counter.compteur_ecritures_deye
last_major_change_entity: input_datetime.deye_last_major_change
p1_sensor: sensor.p1_meter_puissance
deye_grid_sensor: sensor.deye_sunsynk_sol_ark_3_phase_grid_power
voltage_sensor: sensor.deye_sunsynk_sol_ark_3_phase_battery_voltage
soc_sensor: sensor.deye_sunsynk_sol_ark_3_phase_battery_state_of_charge
# --- CONFIGURATION UTILISATEUR ---
start_watts: -200
stable_threshold: 600
# COURBE DE CHARGE
boost_amps: 120 # Max autorisé par TES CÂBLES en Turbo
nominal_amps: 90 # Max recommandé pour la chimie
hardware_limit: 140 # ULTIME SÉCURITÉ (Fusible/Disjoncteur)
# Points de bascule (Switch points)
boost_end_soc: 50 # Fin du Turbo
landing_start_soc: 90 # Début atterrissage
- variables:
current_amps_setting: "{{ states(charge_entity) | float(0) }}"
voltage: "{{ states(voltage_sensor) | float(50) }}"
soc_real: "{{ states(soc_sensor) | float(0) }}"
# STABILITÉ
seconds_since_major_change: >
{% set last_date = states(last_major_change_entity) %}
{% if last_date in ['unknown', 'unavailable', 'none'] %} 0 {% else %}
{{ (as_timestamp(now()) - as_timestamp(last_date)) | float(0) }}
{% endif %}
p1_is_alive: |
{{ states(p1_sensor) not in ['unavailable', 'unknown', 'none']
and (as_timestamp(now()) - as_timestamp(states[p1_sensor].last_updated | default(0)) < 60) }}
grid_watts: |
{% if p1_is_alive %} {{ states(p1_sensor) | float(0) }}
{% else %} {{ states(deye_grid_sensor) | float(0) }}
{% endif %}
# --- 1. SÉCURITÉ RÉSEAU (PANIC) ---
- variables:
grid_previous: >
{% if trigger.platform == 'state' and trigger.from_state is defined %}
{{ trigger.from_state.state | float(0) }}
{% else %}
{{ grid_watts }}
{% endif %}
grid_derivative: "{{ grid_watts - grid_previous }}"
# Panic si dégradation brutale (>1000W)
is_panic_mode: "{{ grid_derivative > 1000 }}"
# --- 2. SÉCURITÉ SYSTÈME (PRÉCISION) ---
- variables:
is_precision_mode: "{{ seconds_since_major_change > stable_threshold and not is_panic_mode }}"
target_grid: "{% if is_precision_mode %} -200 {% else %} -400 {% endif %}"
deadband_amps: "{% if is_precision_mode %} 3 {% else %} 7 {% endif %}"
# --- 3. COURBE DE CHARGE INTELLIGENTE (V17.1) ---
- variables:
# Calcul de la limite théorique selon SOC
# On ajoute une petite hystérésis de 0.5% pour éviter le scintillement aux bornes
curve_limit: >
{# PHASE 1 : TURBO (0% -> 49.5%) #}
{% if soc_real < (boost_end_soc - 0.5) %}
{% set pente = (boost_amps - nominal_amps) / boost_end_soc %}
{{ (boost_amps - (soc_real * pente)) | round(0) }}
{# PHASE 2 : CROISIÈRE (50.5% -> 89.5%) #}
{% elif soc_real < (landing_start_soc - 0.5) %}
{{ nominal_amps }}
{# PHASE 3 : ATTERRISSAGE (90.5% -> 100%) #}
{% else %}
{{ ((100 - soc_real) * 9) | round(0) }}
{% endif %}
# SÉCURITÉ HARD-CAP : On prend le plus petit entre la courbe et la limite matériel
max_amps_dynamic: >
{% if curve_limit > hardware_limit %} {{ hardware_limit }}
{% else %} {{ curve_limit }}
{% endif %}
# Détermination du nom de la phase pour les logs (Cosmétique)
phase_name: >
{% if soc_real < (boost_end_soc - 0.5) %} 🚀 Turbo
{% elif soc_real < (landing_start_soc - 0.5) %} 🛳️ Croisière
{% else %} 🛬 Atterrissage
{% endif %}
# --- CALCUL FINAL ---
- variables:
new_amps_target: >
{# CAS 1 : PANIC #}
{% if is_panic_mode %}
{% if soc_real > 95 or grid_derivative > 3000 %} 0
{% else %} {{ (current_amps_setting / 2) | round(0) }} {% endif %}
{# CAS 2 : Démarrage #}
{% elif current_amps_setting == 0 and grid_watts > start_watts %}
0
{# CAS 3 : Régulation #}
{% else %}
{% set error_watts = grid_watts - target_grid %}
{% set amps_diff = (error_watts / voltage) * -1 %}
{% set calc_target = current_amps_setting + amps_diff %}
{% if calc_target < 3 %} 0
{# Application de la limite dynamique V17 #}
{% elif calc_target > max_amps_dynamic %} {{ max_amps_dynamic }}
{% else %} {{ calc_target | round(0) }}
{% endif %}
{% endif %}
amps_variation: "{{ (new_amps_target - current_amps_setting) | abs }}"
new_soc_target: "{% if new_amps_target > 2 %} 100 {% else %} 15 {% endif %}"
# --- AUTO-DIAGNOSTIC SOC ---
- variables:
entities_group_1: >
{% set res = [] %} {% set target = new_soc_target | float %}
{% if states('number.deye_sunsynk_sol_ark_3_phase_capacity_point_1') | float(-1) != target %} {% set res = res + ['number.deye_sunsynk_sol_ark_3_phase_capacity_point_1'] %} {% endif %}
{% if states('number.deye_sunsynk_sol_ark_3_phase_capacity_point_2') | float(-1) != target %} {% set res = res + ['number.deye_sunsynk_sol_ark_3_phase_capacity_point_2'] %} {% endif %}
{% if states('number.deye_sunsynk_sol_ark_3_phase_capacity_point_3') | float(-1) != target %} {% set res = res + ['number.deye_sunsynk_sol_ark_3_phase_capacity_point_3'] %} {% endif %}
{{ res }}
entities_group_2: >
{% set res = [] %} {% set target = new_soc_target | float %}
{% if states('number.deye_sunsynk_sol_ark_3_phase_capacity_point_4') | float(-1) != target %} {% set res = res + ['number.deye_sunsynk_sol_ark_3_phase_capacity_point_4'] %} {% endif %}
{% if states('number.deye_sunsynk_sol_ark_3_phase_capacity_point_5') | float(-1) != target %} {% set res = res + ['number.deye_sunsynk_sol_ark_3_phase_capacity_point_5'] %} {% endif %}
{% if states('number.deye_sunsynk_sol_ark_3_phase_capacity_point_6') | float(-1) != target %} {% set res = res + ['number.deye_sunsynk_sol_ark_3_phase_capacity_point_6'] %} {% endif %}
{{ res }}
# --- EXECUTION ---
- if:
- condition: template
value_template: >
{% if is_panic_mode %} true
{% elif new_amps_target == 0 and current_amps_setting != 0 %} true
{% elif amps_variation > deadband_amps %} true
{% else %} false {% endif %}
then:
- target:
entity_id: "{{ charge_entity }}"
data:
value: "{{ new_amps_target }}"
action: number.set_value
- target:
entity_id: "{{ write_counter }}"
action: counter.increment
- if:
- condition: template
value_template: "{{ is_panic_mode or (amps_variation > 5 and new_amps_target < current_amps_setting) }}"
then:
- target:
entity_id: "{{ last_major_change_entity }}"
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
action: input_datetime.set_datetime
- data:
name: Deye V17.1
# LOG ULTRA COMPLET POUR SUIVI
message: >
{% if is_panic_mode %}🚨 PANIC (Var: {{ grid_derivative }}W)
{% else %}
{{ phase_name }} | {% if is_precision_mode %}🎯 Précision{% else %}☁️ Standard{% endif %}
{% endif %}
: Modul {{ new_amps_target }}A (Max {{ max_amps_dynamic }}A)
action: logbook.log
# 2. SOC Auto-Healing
- if:
- condition: template
value_template: "{{ entities_group_1 != [] or entities_group_2 != [] }}"
then:
- if:
- condition: template
value_template: "{{ entities_group_1 != [] }}"
then:
- target:
entity_id: "{{ entities_group_1 }}"
data:
value: "{{ new_soc_target }}"
action: number.set_value
- if:
- condition: template
value_template: "{{ entities_group_1 != [] and entities_group_2 != [] }}"
then:
- delay:
seconds: 2
- if:
- condition: template
value_template: "{{ entities_group_2 != [] }}"
then:
- target:
entity_id: "{{ entities_group_2 }}"
data:
value: "{{ new_soc_target }}"
action: number.set_value
- target:
entity_id: "{{ write_counter }}"
action: counter.increment
- data:
name: Deye V17.1
message: "🔧 SOC Fix : {{ entities_group_1 + entities_group_2 }} -> {{ new_soc_target }}%"
action: logbook.log