Skip to content

Instantly share code, notes, and snippets.

@mathieucarbou
Last active November 17, 2025 14:27
Show Gist options
  • Select an option

  • Save mathieucarbou/382556f1279d612962e03232544692d1 to your computer and use it in GitHub Desktop.

Select an option

Save mathieucarbou/382556f1279d612962e03232544692d1 to your computer and use it in GitHub Desktop.
OpenDTU Zero-Injection: Automatically limit inverters based on grid excess power with Home Assistant and OpenDTU up to a configurable Setpoint

OpenDTU Zero-Injection: Automatically limit inverters based on grid excess power with Home Assistant and OpenDTU up to a configurable Setpoint

Capture d’écran 2025-10-04 à 17 23 31

Overview

This HA package allows you to automatically change your inverter production limit to match your home consumption in order to avoid an excessive grid excess.

It also integrates with 3ERL: depending on 3ERL API, the integration will either automaticlaly allow some production excess (when market electricity prices are positive, to make money) or will automaticlally regulate the production limit to avoid any excess when market electricity prices are negative.

Toolbar

image

The toolbar lets you view and control the main configuration and state:

  • Max: the maximum nominal power of the inverters (400W, 500W, 1000W, etc)

  • Limit: the current production limit applied to inverters (10 - Max Watts)

  • Producing: the number of inverters currently producing power

  • 3ERL Trend: 3ERL trend of the day as reported at https://3erl.fr/PREP_Profile.php

  • 3ERL Zero-Inject: status when 3ERL asks to limit the production (see their API at https://3erl.fr/api.json). When asked to limit, this icon becomes red and the production limit will be adjusted to match your home consumption.

  • Setpoint: this is the desired level of excess to try to match. Thsi value is automatically managed but can be manually overriden also for testing.

  • Production: your total production of your solar plant

Left Section - Monitoring

Global Home Power Flowing

image

This cool chart allows you to see how the power is flowing from the grid and from the solar panels to your house. I also added my OpenEVSE car charger,water heater and some other stuff but these are optional.

What is important:

  • The grid voltage: useful to monitor during production spikes to troubleshoot high grid voltage causing inverters to stop
  • The grid power (positive or negative)
  • The produced solar power
  • The maximum voltage of all inverters: inverters are increasing their voltage compared to the grid: the more the powr the more the voltage difference increases, which is more likely to reach the voltage limit when they stop producing and wait

Inverter Power

image

This section allows you to see each inverter AC power output.

The recycle icon allows you to retsart an inverter, which can be useful if it stopped producing for some reaosns and you want to reboot it before it recovers by itself.

Inverter Limit

image

This section displays the live inverter limit for each one and the recycle icon allows you to quickly reset an inverter's limit to 100%.

Right Section - Graphs

The graphs on the right allows to monitor the power, energy, temperature and limits. One of them is special: the PRE graph, which displays the evolution of the electricity marker prices (PRE+ and PRE-) used by 3ERL for monetization.

image

Each graph is grayed out for the period when 3RL asked to limit our production. This is a way to easily see what happens to the power, energy, limit, etc depending if it was a period where we had to limit our production or we were allowing to inject in the grid network.

Installation

OpenDTU

First configure your inverters in OpenDTU with an appropriate naming like Inverter S/N and activate HA integration.

image

You sensors in HA should be something like: sensor.inverter_1410a01e2cdd_*

WARNING

OpenDTU can only send one command at a time and reading inverter data or sending commands take about 1 second per inverter.

So this is a tradeoff between reading data fast and be up to date (OpenDTU polling can be 3 seconds), and have a slower correction time (1 minute), or at the opposite do not mind about readings (DTU polling 10 sec) and favor speed of updating limits (trigger could be 20 sec).

For monitoring purposes and to detect issues I prefer having a fast refresh, so I set my DTU polling to 3 seconds and my update limite rate 1 per minute.

Open OpenDTU console and look for log lines like this:

[14:42:58.575] I (1013189) hoymiles: Queue size - NRF: 0 CMT: 8

These indicate the number of messages sitting in the queue waiting to be processed and you will see the communiction errors. This number should at one point in time go down to 0 or at least be able to go down. You can look also at the last time data was updated in OpeNDTU interface for all inverters. The more you have a big last update time, the more teh DTU is unable to read your inverters.

solar.yaml

Make sure you have configured Home Assistant to support packages.

homeassistant:
  packages: !include_dir_named packages

Put the file solar.yaml in the packages folder:

image

Edit solar.yaml according to your needs.

I have left some TODO comments in the file to indicate what to do.

view.yaml

  • Edit your current dashboard
  • Add a new view
  • Click on the 3 points on the right to display the yaml of the view
  • Paste the content of view.yaml
Capture d’écran 2025-05-29 à 21 32 53

To work properly, you will need to install these frontend plugins from HACS:

  • Power Flow Card Plus
  • Lovelace Entity Progress Card
  • Plotly Graph Card
  • Bar Card
##
# Automatically limit inverters based on grid excess with Home Assistant and OpenDTU
# by Mathieu Carbou
# Ref: https://gist.github.com/mathieucarbou/382556f1279d612962e03232544692d1
##
# https://www.home-assistant.io/integrations/input_number
input_number:
# Inverters nominal power, usually 400W, 500W, 1000W, etc
inverters_nominal_power:
name: Inverters Nominal Power
unit_of_measurement: W
min: 0
max: 2000
step: 1
mode: box
# Target excess or import to stay close to
inverters_excess_setpoint:
name: Inverters Excess Setpoint
unit_of_measurement: W
min: -10000
max: 10000
step: 1
mode: box
# Limit sent to inverters
inverters_power_limit:
name: Inverters Power Limit
unit_of_measurement: W
min: 0
max: 2000
step: 1
mode: box
# https://www.home-assistant.io/integrations/rest
rest:
# Get the 3ERL trend (https://3erl.fr) and limit status and expose them in sensors
- resource: https://3erl.fr/api.json
scan_interval: 60
binary_sensor:
- name: "3ERL: Bridage Demandé"
icon: "mdi:car-speed-limiter"
unique_id: 347A6F4D-6B09-4A20-9C2C-D611ED23EF4B
value_template: "{{ value_json['Bridage'] }}"
sensor:
- name: "3ERL: Tendance du jour"
icon: "mdi:trending-up"
unique_id: 31F1BED4-8100-44A4-88BA-DB45450611CF
# multiply by 10 because the value is in c€/kWh
value_template: |
{% set data = value_json['PREP_Profile'] %}
{% if data == '3+' %} 🟢🟢🟢
{% elif data == '2+' %} 🟢🟢
{% elif data == '1+' %} 🟢
{% elif data == '0' %} ⚠️
{% elif data == '1-' %} ⛔️
{% elif data == '2-' %} ⛔️⛔️
{% elif data == '3-' %} ⛔️⛔️⛔️
{% else %} ⁉️
{% endif %}
# Get the electricity market price trend from RTE for the PRE graph
- resource_template: https://www.services-rte.com/cms/open_data/v1/price/table?startDate={{ now().strftime('%d%%2F%m%%2F%Y') }}
scan_interval: 60
sensor:
- name: "Électricité: Prix de règlement des écarts positifs"
icon: "mdi:currency-eur"
unique_id: EA2D2A8C-5327-47C8-92EF-C16E9BE7A4C5
state_class: measurement
unit_of_measurement: "€/MWh"
value_template: "{{ value_json['values'][0]['pre']['positive'] | float }}"
- name: "Électricité: Prix de règlement des écarts négatifs"
icon: "mdi:currency-eur"
unique_id: 95AE2165-7E9A-40BA-A555-1C7B7B93B26D
value_template: "{{ value_json['values'][0]['pre']['negative'] | float }}"
state_class: measurement
unit_of_measurement: "€/MWh"
template:
# https://www.home-assistant.io/integrations/sensor/
- sensor:
# Compute the number of inverters currently producing power
# !! TODO !! Put your inverter S/N in this list in lowercase, any order
- name: Inverters Producing Count
unique_id: 7FAAECB4-2105-4F86-B63E-A1962F7FC81E
state: >-
{{ ['1410a01ec916', '1410a01ed6ca', '1410a01ed5a9', '1410a01ed5f0', '1410a01f092d', '1410a01ed604', '1410a01f010c', '1410a01e2cdd']|map('regex_replace', '(.+)', "sensor.inverter_\\1_power")|map('states')|map('float', 0)|select('gt', 0)|list|count }}
# Total Solar Production Power
# !! TODO !! Make sure you have a sensor called Solar Production Power which reflects your total plant production power.
# In my case, I have 2 production groups, so I create a sensor which summarizes both here.
- name: Solar Production Power
unique_id: "01e37eed-3045-4eee-a786-1249567fe300"
state_class: measurement
device_class: power
unit_of_measurement: W
state: "{{ states.sensor.solar_plant_group_a_power.state|float(0) + states.sensor.solar_plant_group_b_power.state|float(0) }}"
# Home Consumed Power: power consumed by your house == the produced power plus the measured grid power
# !! TODO !!configure your sensor measuring your grid power here. Mine is a Shelly and is called: sensor.grid_power
- name: Home Consumed Power
unique_id: BE34D1AD-AB8E-4909-ACE0-BBA7D3877105
state_class: measurement
device_class: power
unit_of_measurement: W
availability: "{{ ['sensor.solar_production_power', 'sensor.grid_power']|map('states')|map('is_number')|min }}"
state: "{{ states.sensor.solar_production_power.state|float + states.sensor.grid_power.state|float }}"
# Grid Returned Power: this is the gros power returned to the grid, or 0 if no excess
# !! TODO !! configure your sensor measuring your grid power here. Mine is a Shelly and is called: sensor.grid_power
- name: Grid Returned Power
unique_id: "01e37eed-3045-4eee-a786-1249567fe214"
state_class: measurement
device_class: power
unit_of_measurement: W
availability: "{{ ['sensor.grid_power']|map('states')|map('is_number')|min }}"
state: "{{ 0 - [states.sensor.grid_power.state|float, 0] | min }}"
# Solar Consumed Power: This is the produced power consumed by your house
- name: Solar Consumed Power
unique_id: "01e37eed-3045-4eee-a786-1249567fe303"
state_class: measurement
device_class: power
unit_of_measurement: W
availability: "{{ ['sensor.solar_production_power', 'sensor.grid_returned_power']|map('states')|map('is_number')|min }}"
state: "{{ [states.sensor.solar_production_power.state|float - states.sensor.grid_returned_power.state|float, 0] | max }}"
# This sensor tries to calculate the daily consumed energy by your house, which is the produced energy + grid imported energy
# c == this sensor last value (consumed solar production)
# p == daily solar production
# e == daily return to grid (loss)
# we should always have: p >= c and p >= e
# At midnight, yield day resets but not at the same time so we can have p < c or p < e
# When not at midnight, we compare last value of c with newly computed value (c == p - e)
- name: Solar Consumed Energy Daily
unique_id: "01e37eed-3045-4eee-a786-1249567fe305"
state_class: total_increasing
device_class: energy
unit_of_measurement: kWh
state: >-
{% set p = states.sensor.inverters_energy_meter_daily.state|float(0) %}
{% set c = states.sensor.solar_consumed_energy_daily.state|float(0) %}
{% set e = states.sensor.grid_energy_returned_meter_daily.state|float(0) %}
{{ [0 if p == 0 or p < c or p < e else c, [0, p - e] | max] | max }}
# This sensor computes your autoconumption ratio
# This data is measleading through when you are registered to 3ERL, because sometimes you need to reach a 100% ratio, and sometimes you do not have to, in order to make money.
- name: Solar Autoconsumption Level Daily
unique_id: "01e37eed-3045-4eee-a786-1249567fe304"
state_class: measurement
unit_of_measurement: "%"
availability: "{{ states('sensor.inverters_energy_meter_daily')|float(0) > 0 }}"
state: "{{ [100, (100 - 100 * states('sensor.grid_energy_returned_meter_daily')|float(0) / states('sensor.inverters_energy_meter_daily')|float )] | min }}"
sensor:
# https://www.home-assistant.io/integrations/integration/
# Solar Production Energy: accumulated energy of your solar plant
# This could be your Shelly if you have a simple setup, but since I have several groups, I am computing the energy based on my total produced power
- platform: integration
name: Solar Production Energy
unique_id: E85A7E482-7937-4ABC-A0C6-1B9790DF417F
source: sensor.solar_production_power
unit_prefix: k
# Home Consumed Energy
- platform: integration
name: Home Consumed Energy
unique_id: E2BA4424-8CA8-4D74-9A2F-F0E5EFF84622
source: sensor.home_consumed_power
unit_prefix: k
# https://www.home-assistant.io/integrations/utility_meter/
utility_meter:
# Grid Energy Returned Meter Daily: Daily meter for the grid returned energy.
# Mine comes from a Linky key which is far more reliable than a Shelly since reads the counters directly stored in the Linky
# !! TODO !! set your grid returned energy sensor here
grid_energy_returned_meter_daily:
name: Grid Energy Returned Meter Daily
unique_id: 454B7731-375D-4C90-AFC0-42EAB23DF11D
source: sensor.linky_energie_injectee
always_available: true
cycle: daily
# Inverters Energy meter
# !! TODO !! set your OpenDTU YieldTotal sensor
inverters_energy:
name: Inverters Energy
unique_id: 6E14EC86-0965-463E-BFBF-6008B61B56AF
source: sensor.opendtu_ff4930_yield_total
always_available: true
# Inverters Energy Meter Daily
# !! TODO !! set your OpenDTU YieldTotal sensor
inverters_energy_meter_daily:
name: Inverters Energy Meter Daily
unique_id: 4C4D8D06-C9D2-4408-B21A-1274A6E0F041
source: sensor.opendtu_ff4930_yield_total
always_available: true
cycle: daily
# Home Consumed Energy Meter Daily: you daily home consumption
home_consumed_energy_meter_daily:
name: Home Consumed Energy Meter Daily
unique_id: 41AAA458-7545-462F-8E78-FD1D037A8DBE
source: sensor.home_consumed_energy
always_available: true
cycle: daily
# https://www.home-assistant.io/docs/automation/
automation:
# Automatically propagate changes in power limit input number to inverters
# !! TODO !! set your minimal production limit (minimalPowerLimit): this should be such that minimalPowerLimit * number of inverters == minimal home consumption
- id: "0000000000038"
alias: "Solar: Update Inverter Power Limit"
trigger:
- trigger: state
entity_id:
- input_number.inverters_power_limit
condition: []
action:
- action: number.set_value
data:
value: >-
{% set minimalPowerLimit = 100 %}
{% set nominalPower = states('input_number.inverters_nominal_power')|int(minimalPowerLimit) %}
{% set powerLimit = states('input_number.inverters_power_limit')|float(nominalPower) %}
{{ powerLimit / nominalPower * 100.0 }}
target:
entity_id:
- number.inverter_1410a01e2cdd_limit_nonpersistent_relative
- number.inverter_1410a01ec916_limit_nonpersistent_relative
- number.inverter_1410a01ed6ca_limit_nonpersistent_relative
- number.inverter_1410a01ed5a9_limit_nonpersistent_relative
- number.inverter_1410a01ed5f0_limit_nonpersistent_relative
- number.inverter_1410a01f092d_limit_nonpersistent_relative
- number.inverter_1410a01ed604_limit_nonpersistent_relative
- number.inverter_1410a01f010c_limit_nonpersistent_relative
# Runs at a frequent interval to update the inverters limit
# !! TODO !! set your Grid Power sensor for `sensor.grid_power`
# !! TODO !! update the list of entity IDs matching your inverters S/N: these sensors come from OpenDTU
# !! TODO !! set your minimal production limit (minimalPowerLimit): this should be such that minimalPowerLimit * number of inverters == minimal home consumption
- id: "0000000000039"
alias: "Solar: Auto update power limits"
trigger:
- trigger: time_pattern
minutes: /1
condition:
- condition: state
entity_id: binary_sensor.opendtu_ff4930_status
state: "on"
- condition: numeric_state
entity_id: sensor.inverters_producing_count
above: 0
action:
- action: input_number.set_value
data:
value: >-
{% set minimalPowerLimit = 100 %}
{% set grid = states('sensor.grid_power')|float(0) %}
{% set setpoint = states('input_number.inverters_excess_setpoint')|int(0) %}
{% set nominalPower = states('input_number.inverters_nominal_power')|int(minimalPowerLimit) %}
{% set powerLimit = states('input_number.inverters_power_limit')|float(nominalPower) %}
{% set producing = states('sensor.inverters_producing_count')|int(0) %}
{% set missedPower = (grid - setpoint) / producing if producing > 0 else nominalPower %}
{{ [nominalPower, [minimalPowerLimit, powerLimit + missedPower|round(0, "ceil")]|max]|min }}
target:
entity_id:
- input_number.inverters_power_limit
# Update the setpoint depending on events
# !! TODO !! set your events accordingly. For example, `binary_sensor.openevse_vehicle_connected` and `sensor.openevse_vehicle_battery_level` and `binary_sensor.openevse_vehicle_charge` are for my EV car charger. You might not need them.
# !! TODO !! update the conditions: here, I need a special condition that sets the setpoint to -600 when my EV si connected so that it has enough excess to start charging. If you do not have an EV, then put -200 or -100 or 0.
# The value of -5500 is because with single phase in France you cannot divert to the grid more than 6kVA. So this is a safety value in case your home is consuming less, the script will reduce the soalr production ot make sure it does not feed teh grid with more than 5.5kW.
- id: "0000000000041"
alias: "Solar: Auto update Setpoint"
triggers:
- trigger: state
entity_id:
- binary_sensor.3erl_bridage_demande
- binary_sensor.openevse_vehicle_connected
- sensor.openevse_vehicle_battery_level
- binary_sensor.openevse_vehicle_charge
- trigger: homeassistant
event: start
conditions: []
actions:
- if:
- condition: state
entity_id: binary_sensor.3erl_bridage_demande
state: "on"
then:
- if:
- condition: state
entity_id: binary_sensor.openevse_vehicle_connected
state: "on"
- condition: numeric_state
entity_id: sensor.openevse_vehicle_battery_level
below: 100
then:
- action: input_number.set_value
metadata: {}
data:
value: -600
target:
entity_id: input_number.inverters_excess_setpoint
else:
- action: input_number.set_value
metadata: {}
data:
value: -200
target:
entity_id: input_number.inverters_excess_setpoint
- if:
- condition: state
entity_id: binary_sensor.3erl_bridage_demande
state: "off"
then:
- action: input_number.set_value
metadata: {}
data:
value: -5500
target:
entity_id: input_number.inverters_excess_setpoint
# Notification in case a high grid volatge is detected
# This is optional and require you to know how to setup HA notifications.
- id: "0000000000042"
alias: "Solar: Notify of high grid voltage"
trigger:
- trigger: numeric_state
entity_id:
- sensor.grid_voltage
above: 250
action:
- action: notify.whatsapp_mathieu
data:
message: "[GRID] High Voltage (> 250V)"
- delay:
hours: 0
minutes: 1
seconds: 0
milliseconds: 0
mode: single
max_exceeded: silent
# Notification in case the number of inverters producing changes.
# This helps detect inverters which are dropping because if grid high voltage for example.
# This is optional and require you to know how to setup HA notifications.
- id: "0000000000043"
alias: "Solar: Notify of inverter producing count"
trigger:
- trigger: state
entity_id:
- sensor.inverters_producing_count
to: null
action:
- action: notify.whatsapp_mathieu
data:
message: "[SOLAR] Inverters producing: {{ states('sensor.inverters_producing_count')|int(0) }}"
mode: single
max_exceeded: silent
# Notification in case 3ERL changes its zero-inject order
# This is optional and require you to know how to setup HA notifications.
- id: "0000000000040"
alias: "Solar: Notify 3RL State Change"
triggers:
- trigger: state
entity_id:
- binary_sensor.3erl_bridage_demande
to: "on"
- trigger: state
entity_id:
- binary_sensor.3erl_bridage_demande
from: "on"
to: "off"
- trigger: homeassistant
event: start
conditions: []
actions:
- if:
- condition: state
entity_id: binary_sensor.3erl_bridage_demande
state: "on"
then:
- action: notify.whatsapp_mathieu
data:
message: "[SOLAR] 3RL Bridage: ON"
- if:
- condition: state
entity_id: binary_sensor.3erl_bridage_demande
state: "off"
then:
- action: notify.whatsapp_mathieu
data:
message: "[SOLAR] 3RL Bridage: OFF"
type: sections
max_columns: 3
title: Solaire
path: solar
icon: mdi:solar-panel-large
sections:
- type: grid
cards:
- type: custom:power-flow-card-plus
entities:
grid:
entity: sensor.grid_power
display_state: one_way
secondary_info:
entity: sensor.grid_voltage
unit_of_measurement: V
display_zero: true
color_value: false
decimals: 1
use_metadata: false
color_value: false
color_icon: true
solar:
entity: sensor.solar_production_power
display_zero_state: true
color_value: false
color_icon: true
secondary_info:
entity: sensor.max_inverter_voltage
unit_of_measurement: V
display_zero: true
color_value: false
decimals: 1
home:
entity: sensor.home_consumed_power
color_value: false
color_icon: true
secondary_info: {}
individual:
- entity: sensor.openevse_power
display_zero: true
icon: mdi:car-electric
name: OpenEVSE
color_value: false
color_icon: true
secondary_info:
entity: sensor.renault_zoe_battery_level
unit_of_measurement: "%"
display_zero: true
unit_white_space: true
display_zero_state: true
unit_white_space: true
- entity: sensor.chauffe_eau_power
display_zero: true
color_value: false
color_icon: true
secondary_info: {}
name: Chauffe-eau
icon: mdi:water-boiler
calculate_flow_rate: false
display_zero_state: true
unit_white_space: true
- entity: sensor.total_deshumidificateur_power
display_zero: true
color_value: false
color_icon: true
secondary_info: {}
name: Déshumidificateurs
icon: mdi:air-filter
display_zero_state: true
calculate_flow_rate: false
unit_white_space: true
- entity: sensor.total_pac_power
display_zero: true
color_value: false
color_icon: true
secondary_info: {}
name: PAC
icon: mdi:air-conditioner
display_zero_state: true
unit_white_space: true
clickable_entities: true
display_zero_lines:
mode: show
transparency: 50
grey_color:
- 189
- 189
- 189
use_new_flow_rate_model: true
max_expected_power: 12000
watt_threshold: 12000
kw_decimals: 2
min_flow_rate: 0.5
max_flow_rate: 3
- type: entities
entities:
- type: custom:entity-progress-card
entity: sensor.inverter_1410a01e2cdd_power
decimal: 0
min_value: 0
max_value: 1000
name: "#1: 1410A01E2CDD"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: button.inverter_1410a01e2cdd_restart_inverter
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: sensor.inverter_1410a01ec916_power
decimal: 0
min_value: 0
max_value: 1000
name: "#2: 1410A01EC916"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: button.inverter_1410a01ec916_restart_inverter
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: sensor.inverter_1410a01ed6ca_power
decimal: 0
min_value: 0
max_value: 1000
name: "#3: 1410A01ED6CA"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: button.inverter_1410a01ed6ca_restart_inverter
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: sensor.inverter_1410a01ed5a9_power
decimal: 0
min_value: 0
max_value: 1000
name: "#4: 1410A01ED5A9"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: button.inverter_1410a01ed5a9_restart_inverter
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: sensor.inverter_1410a01ed5f0_power
decimal: 0
min_value: 0
max_value: 1000
name: "#5: 1410A01ED5F0"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: button.inverter_1410a01ed5f0_restart_inverter
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: sensor.inverter_1410a01f092d_power
decimal: 0
min_value: 0
max_value: 1000
name: "#7: 1410A01F092D"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: button.inverter_1410a01f092d_restart_inverter
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: sensor.inverter_1410a01ed604_power
decimal: 0
min_value: 0
max_value: 1000
name: "#8: 1410A01ED604"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: button.inverter_1410a01ed604_restart_inverter
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: sensor.inverter_1410a01f010c_power
decimal: 0
min_value: 0
max_value: 1000
name: "#9: 1410A01F010C"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: button.press
target:
entity_id: button.inverter_1410a01f010c_restart_inverter
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
title: Inverter Power
- type: entities
entities:
- type: custom:entity-progress-card
entity: number.inverter_1410a01e2cdd_limit_nonpersistent_relative
decimal: 1
name: "#1: 1410A01E2CDD"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: number.set_value
data:
value: 100
target:
entity_id: number.inverter_1410a01e2cdd_limit_nonpersistent_relative
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: number.inverter_1410a01ec916_limit_nonpersistent_relative
decimal: 1
name: "#2: 1410A01EC916"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: number.set_value
data:
value: 100
target:
entity_id: number.inverter_1410a01ec916_limit_nonpersistent_relative
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: number.inverter_1410a01ed6ca_limit_nonpersistent_relative
decimal: 1
name: "#3: 1410A01ED6CA"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: number.set_value
data:
value: 100
target:
entity_id: number.inverter_1410a01ed6ca_limit_nonpersistent_relative
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: number.inverter_1410a01ed5a9_limit_nonpersistent_relative
decimal: 1
name: "#4: 1410A01ED5A9"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: number.set_value
data:
value: 100
target:
entity_id: number.inverter_1410a01ed5a9_limit_nonpersistent_relative
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: number.inverter_1410a01ed5f0_limit_nonpersistent_relative
decimal: 1
name: "#5: 1410A01ED5F0"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: number.set_value
data:
value: 100
target:
entity_id: number.inverter_1410a01ed5f0_limit_nonpersistent_relative
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: number.inverter_1410a01f092d_limit_nonpersistent_relative
decimal: 1
name: "#7: 1410A01F092D"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: number.set_value
data:
value: 100
target:
entity_id: number.inverter_1410a01f092d_limit_nonpersistent_relative
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: number.inverter_1410a01ed604_limit_nonpersistent_relative
decimal: 1
name: "#8: 1410A01ED604"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: number.set_value
data:
value: 100
target:
entity_id: number.inverter_1410a01ed604_limit_nonpersistent_relative
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
- type: custom:entity-progress-card
entity: number.inverter_1410a01f010c_limit_nonpersistent_relative
decimal: 1
name: "#9: 1410A01F010C"
hold_action:
action: more-info
tap_action:
action: perform-action
perform_action: number.set_value
data:
value: 100
target:
entity_id: number.inverter_1410a01f010c_limit_nonpersistent_relative
layout: horizontal
force_circular_background: true
bar_size: medium
icon: mdi:restart
theme: optimal_when_high
grid_options:
columns: 12
rows: 1
title: Inverter Limit
- type: custom:bar-card
title: Inverter Voltage
direction: right
height: 25
positions:
icon: "off"
indicator: inside
name: inside
value: inside
entities:
- entity: sensor.inverter_1410a01e2cdd_voltage
name: "#1: 1410A01E2CDD"
- entity: sensor.inverter_1410a01ec916_voltage
name: "#2: 1410A01EC916"
- entity: sensor.inverter_1410a01ed6ca_voltage
name: "#3: 1410A01ED6CA"
- entity: sensor.inverter_1410a01ed5a9_voltage
name: "#4: 1410A01ED5A9"
- entity: sensor.inverter_1410a01ed5f0_voltage
name: "#5: 1410A01ED5F0"
- entity: sensor.inverter_1410a01f092d_voltage
name: "#7: 1410A01F092D"
- entity: sensor.inverter_1410a01ed604_voltage
name: "#8: 1410A01ED604"
- entity: sensor.inverter_1410a01f010c_voltage
name: "#9: 1410A01F010C"
min: 230
max: 253
- type: custom:bar-card
title: Inverter Temperature
direction: right
height: 25px
positions:
icon: "off"
indicator: inside
name: inside
value: inside
entities:
- entity: sensor.inverter_1410a01e2cdd_temperature
name: "#1: 1410A01E2CDD"
- entity: sensor.inverter_1410a01ec916_temperature
name: "#2: 1410A01EC916"
- entity: sensor.inverter_1410a01ed6ca_temperature
name: "#3: 1410A01ED6CA"
- entity: sensor.inverter_1410a01ed5a9_temperature
name: "#4: 1410A01ED5A9"
- entity: sensor.inverter_1410a01ed5f0_temperature
name: "#5: 1410A01ED5F0"
- entity: sensor.inverter_1410a01f092d_temperature
name: "#7: 1410A01F092D"
- entity: sensor.inverter_1410a01ed604_temperature
name: "#8: 1410A01ED604"
- entity: sensor.inverter_1410a01f010c_temperature
name: "#9: 1410A01F010C"
min: 0
max: 80
- type: grid
cards:
- type: horizontal-stack
grid_options:
columns: full
cards:
- type: custom:plotly-graph
title: Grid and Solar Power
refresh_interval: 60
hours_to_show: 8
layout:
yaxis9:
visible: false
fixedrange: true
xaxis:
rangeselector:
"y": 1.2
bgcolor: grey
buttons:
- count: 1
step: hour
- count: 2
step: hour
- count: 4
step: hour
- count: 8
step: hour
- count: 12
step: hour
entities:
- entity: sensor.home_consumed_power
name: Consumption
line:
color: "#A52A2A"
width: 1
- entity: sensor.openevse_power
name: OpenEVSE
line:
color: "#00AA00"
width: 1
- entity: sensor.grid_imported_power
name: Grid
line:
color: "#0000FF"
width: 1
- entity: sensor.solar_production_power
name: Solar
line:
color: "#ff8c00"
width: 1
- entity: sensor.grid_returned_power
name: Excess
line:
color: "#FFFF00"
width: 1
filters:
- multiply: -1
- entity: binary_sensor.3erl_bridage_demande
name: Bridage
showlegend: true
yaxis: y9
filters:
- map_y: "y === 'on' ? 1 : 0"
fill: tozeroy
line:
color: grey
shape: hv
connectgaps: false
width: 0
- type: horizontal-stack
cards:
- type: custom:plotly-graph
title: PRE
refresh_interval: 60s
hours_to_show: 8
defaults:
entity:
connectgaps: true
line:
shape: spline
layout:
yaxis9:
visible: false
fixedrange: true
xaxis:
rangeselector:
"y": 1.2
bgcolor: grey
buttons:
- count: 1
step: hour
- count: 2
step: hour
- count: 4
step: hour
- count: 8
step: hour
- count: 12
step: hour
entities:
- entity: sensor.electricite_prix_de_reglement_des_ecarts_negatifs
name: PRE-
- entity: sensor.electricite_prix_de_reglement_des_ecarts_positifs
name: PRE+
- entity: binary_sensor.3erl_bridage_demande
name: Bridage
showlegend: true
yaxis: y9
filters:
- map_y: "y === 'on' ? 1 : 0"
fill: tozeroy
line:
color: grey
shape: hv
connectgaps: false
width: 0
grid_options:
columns: full
- type: horizontal-stack
cards:
- type: custom:plotly-graph
title: Energy
refresh_interval: 1m
hours_to_show: 8
layout:
barmode: relative
yaxis9:
visible: false
fixedrange: true
xaxis:
rangeselector:
"y": 1.2
bgcolor: grey
buttons:
- count: 8
step: hour
- count: 12
step: hour
- count: 1
step: day
- count: 7
step: day
entities:
- entity: sensor.inverters_energy
name: Solar
statistic: sum
type: bar
unit_of_measurement: Wh
period: auto
marker:
color: "#ff8c00"
filters:
- multiply: 1000
- delta
- entity: sensor.grid_total_active_energy
name: Grid
statistic: sum
type: bar
unit_of_measurement: Wh
period: auto
marker:
color: "#0000FF"
filters:
- multiply: 1000
- delta
- entity: sensor.home_consumed_energy
name: Consumption
statistic: sum
type: bar
unit_of_measurement: Wh
period: auto
marker:
color: "#A52A2A"
filters:
- multiply: -1000
- delta
- entity: sensor.linky_energie_injectee
name: Excess
statistic: sum
type: bar
unit_of_measurement: Wh
period: auto
marker:
color: "#FFFF00"
filters:
- multiply: -1000
- delta
- entity: sensor.openevse_total_active_energy
name: OpenEVSE
statistic: sum
unit_of_measurement: Wh
period: auto
type: line
line:
color: "#00FF00"
width: 1
filters:
- multiply: 1000
- delta
- entity: binary_sensor.3erl_bridage_demande
name: Bridage
showlegend: true
yaxis: y9
filters:
- map_y: "y === 'on' ? 1 : 0"
fill: tozeroy
line:
color: grey
shape: hv
connectgaps: false
width: 0
grid_options:
columns: full
- type: horizontal-stack
cards:
- type: custom:plotly-graph
title: Inverter Limit
refresh_interval: 60
hours_to_show: 8
layout:
barmode: relative
yaxis:
autorange: max
rangemode: tozero
yaxis9:
visible: false
fixedrange: true
xaxis:
rangeselector:
"y": 1.2
bgcolor: grey
buttons:
- count: 1
step: hour
- count: 2
step: hour
- count: 4
step: hour
- count: 8
step: hour
- count: 12
step: hour
entities:
- entity: input_number.inverters_power_limit
- entity: binary_sensor.3erl_bridage_demande
name: Bridage
showlegend: true
yaxis: y9
filters:
- map_y: "y === 'on' ? 1 : 0"
fill: tozeroy
line:
color: grey
shape: hv
connectgaps: false
width: 0
grid_options:
columns: 24
rows: auto
- type: horizontal-stack
cards:
- type: custom:plotly-graph
title: Inverter Temperatures
refresh_interval: 60
hours_to_show: 8
layout:
barmode: relative
yaxis:
autorange: max
rangemode: tozero
yaxis9:
visible: false
fixedrange: true
xaxis:
rangeselector:
"y": 1.2
bgcolor: grey
buttons:
- count: 1
step: hour
- count: 2
step: hour
- count: 4
step: hour
- count: 8
step: hour
- count: 12
step: hour
entities:
- entity: sensor.inverter_1410a01e2cdd_temperature
name: "#1"
- entity: sensor.inverter_1410a01ec916_temperature
name: "#2"
- entity: sensor.inverter_1410a01ed6ca_temperature
name: "#3"
- entity: sensor.inverter_1410a01ed5a9_temperature
name: "#4"
- entity: sensor.inverter_1410a01ed5f0_temperature
name: "#5"
- entity: sensor.inverter_1410a01f092d_temperature
name: "#7"
- entity: sensor.inverter_1410a01ed604_temperature
name: "#8"
- entity: sensor.inverter_1410a01f010c_temperature
name: "#9"
- entity: binary_sensor.3erl_bridage_demande
name: Bridage
showlegend: true
yaxis: y9
filters:
- map_y: "y === 'on' ? 1 : 0"
fill: tozeroy
line:
color: grey
shape: hv
connectgaps: false
width: 0
grid_options:
columns: full
- type: horizontal-stack
cards:
- type: custom:plotly-graph
title: Energy (hourly)
refresh_interval: 5m
hours_to_show: 7d
layout:
barmode: relative
yaxis9:
visible: false
fixedrange: true
xaxis:
rangeselector:
"y": 1.2
bgcolor: grey
buttons:
- count: 1
step: day
- count: 3
step: day
- count: 7
step: day
entities:
- entity: sensor.inverters_energy
name: Solar
statistic: sum
type: bar
unit_of_measurement: kWh
period: hour
marker:
color: "#ff8c00"
filters:
- multiply: 1
- delta
- entity: sensor.grid_total_active_energy
name: Grid
statistic: sum
type: bar
unit_of_measurement: kWh
period: hour
marker:
color: "#0000FF"
filters:
- multiply: 1
- delta
- entity: sensor.home_consumed_energy
name: Consumption
statistic: sum
type: bar
unit_of_measurement: kWh
period: hour
marker:
color: "#A52A2A"
filters:
- multiply: -1
- delta
- entity: sensor.linky_energie_injectee
name: Excess
statistic: sum
type: bar
unit_of_measurement: kWh
period: hour
marker:
color: "#FFFF00"
filters:
- multiply: -1
- delta
- entity: sensor.openevse_total_active_energy
name: OpenEVSE
statistic: sum
unit_of_measurement: kWh
period: hour
type: line
line:
color: "#00FF00"
width: 1
filters:
- multiply: 1
- delta
- entity: binary_sensor.3erl_bridage_demande
name: Bridage
showlegend: true
yaxis: y9
filters:
- map_y: "y === 'on' ? 1 : 0"
fill: tozeroy
line:
color: grey
shape: hv
connectgaps: false
width: 0
grid_options:
columns: full
column_span: 2
cards: []
badges:
- type: entity
show_name: true
show_state: true
show_icon: true
entity: input_number.inverters_nominal_power
name: Max
- type: entity
show_name: true
show_state: true
show_icon: true
entity: input_number.inverters_power_limit
name: Limit
- type: entity
show_name: true
show_state: true
show_icon: true
entity: sensor.inverters_producing_count
name: Producing
- type: entity
show_name: true
show_state: true
show_icon: true
entity: sensor.3erl_tendance_du_jour
name: 3ERL Trend
- type: entity
show_name: true
show_state: false
show_icon: true
entity: binary_sensor.3erl_bridage_demande
name: 3ERL Zero-Inject
color: red
icon: mdi:cancel
- type: entity
show_name: true
show_state: true
show_icon: true
entity: input_number.inverters_excess_setpoint
icon: mdi:bullseye-arrow
name: Setpoint
- type: entity
show_name: true
show_state: true
show_icon: true
entity: sensor.inverters_energy
name: Production
visible:
- user: 8b29665394c74237a7759b784fc5232e
@mathieucarbou
Copy link
Author

  • Bridage up to 14h00
  • 12h00: VE goes out => limiter auto-limit production to lower export
  • 13h00: VE goes back to charge => limiter update production

Screenshot 2025-0
3-09 at 18 28 19

@Mancier34
Copy link

Mancier34 commented May 1, 2025

Salut ! Vraiment très intéressante cette création ! Ayant actuellement 3,5kWc d’onduleurs hoymiles, un openDTU que j’ai confectionné, un home assistant sur un nuc, ainsi qu’un routeur solaire, je me heurt désormais à un petit soucis. Avec les beaux jours je me retrouve à faire pas mal d’injection sur le réseau…
Ta solution me semble parfaite dans mon cas mais pas simple à mettre en place, aurais-tu un " tuto " pas à pas pour faire la mise en place sur HA ? Ou encore mieux une vidéo YouTube ?
il faut copier / coller le fichier " opendtu.yaml " dans le fichier " configuration.yaml " de mon home assistant ? ( bien sûr en mettant à jour les différents sensors )
merci par avance de ton aide

@Mancier34
Copy link

Pas si simple que cela pour un novice en automatisation pour mettre la solution en place. Un petit coup de pouce ne serait pas de trop 😅

@T4rb6
Copy link

T4rb6 commented May 5, 2025

Meme un peu d'aide ne serait pas de refus j'ai tous mis dans config package comme explique plus bas dans le gith mais ca fonctionne pas

@mathieucarbou
Copy link
Author

mathieucarbou commented May 5, 2025

Les packages sont un concept HA pour séparer la config en modules:

image image

Il suffit de mettre en place pour ensuite isoler ses modules dans un dossier packages.

Voir: https://www.home-assistant.io/docs/configuration/packages/#create-a-packages-folder => "Create a packages folder "

@T4rb6
Copy link

T4rb6 commented May 6, 2025

ok merci
j'avais déjà crée la ligne packages dans configuration.yaml
opendtu.yaml se charge je pense car j'ai les capteurs qui se sont crée dans l'aperçu général (non fonctionnel pour la plupart car je n'ai pas modifie leurs id dans votre script)
par contre view.yaml et opendtu_3erl.yaml ne se charge pas ou je ne sait pas par ou y accéder
j'ai la vérifié les .yaml dans outils de développement HA ne trouve pas d'erreur ...

@T4rb6
Copy link

T4rb6 commented May 6, 2025

je vois dans votre copie d'ecran que vous n'avez pas view.yaml dans le dossier package. Il doit se placer ailleurs ?

@mathieucarbou
Copy link
Author

view.yaml c'est le yaml de la vue dashboard: quand on crée une section du dashboard, on peut aller voir son code yaml dans les options. C est ce yaml là.

@T4rb6
Copy link

T4rb6 commented May 6, 2025

ok j'ai crée un Dashboard supplémentaire view
mais quand je copie colle votre code dans l'editeur de configuration j'ai cette erreur a l'enregistrement ....
"Votre configuration n'est pas valide : r: At path: views -- Expected an array value, but received: undefined"....

@mathieucarbou
Copy link
Author

I have updated the gist.

@mathieucarbou
Copy link
Author

I have update the files and doc. Good luck 😇😗🤭

@cedrichuchette
Copy link

Hello,

thanks for the job done! It is exactly what I need as to day I inject too much power on the grid for free and looked up to 3ERL but was wondering how to block my inverters to avoid injecting on negative hours...

The issue is that I have Deye micro inverters so I can't use openDTU but I would be able still to integrate them with Solarman integration.
And I have Anker sensor as well integrated on HA not Shelly.

Do you think it would be complicated to use your integration and adapt?

I am not so familiar with HA but can work on it...

thanks in advance

@mathieucarbou
Copy link
Author

mathieucarbou commented Oct 6, 2025

Do you think it would be complicated to use your integration and adapt?

The first thing you need to check is:

  1. Can you set some power limits on your Deye inverters through an API ?
  2. Can you get your inverter settings (produced power, applied limit, total energy, temperature, etc) from an API ?
  3. If yes, are you able to you call these API from HA ?

If you can do all three, then you are able to setup sensors nearly the same way it is done for the 3ERL and PRE calls and be able to control your inverters from HA.

There seem to be an HA integration for Deye: https://kellerza.github.io/sunsynk/guide/tested-inverters

If that works, it is probably the easiest option since you could just replace OpenDTU sensors by yours.

I am not so familiar with HA but can work on it...

The only complex things is to wire Deye and replace OpenDTU sensors by yours. The remaining logic stays the same. This is complex though and you'll need to ramp up probably your HA knowledge on how to wire that.

Start small, add stuff step by step.

It took me 6 months to refine this integration up to where it is now. I started in March or May this year and revised it often. So don't think big first, just try to first read your inverter data from HA, then try control them, etc etc.

@mathieucarbou
Copy link
Author

Updates:

  • improved PID reaction
  • switch from sensor to input number (user can change the limit)
  • decrease update delay to 30 sec
  • avoids sending an update to opendtu if limit did not change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment