Skip to content

Instantly share code, notes, and snippets.

@mathieucarbou
Last active March 6, 2026 08:33
Show Gist options
  • Select an option

  • Save mathieucarbou/8d83d25247821e85a693dea61fe4f0d2 to your computer and use it in GitHub Desktop.

Select an option

Save mathieucarbou/8d83d25247821e85a693dea61fe4f0d2 to your computer and use it in GitHub Desktop.
3ERL Zero-Inject Instructions and Daily PRE+ Estimate

3ERL Zero-Inject Instructions and Daily PRE+ Estimate

Capture d’écran 2026-03-02 à 22 41 14

Overview

This HA package allows you to follow the PRE+ prices and 3ERL Zero-Inject instruction and also override it. It will display 2 graphs for 2 modes.

"ACI" mode (default mode when subscribing to 3ERL)

image

This graph displays:

  • PRD3 sunlight profile from the selected day in the past
  • PRE+ values for each 1/4h
  • PRE+ value adjusted with the PRD3 factor of the same 1/4h
  • The currently estimated daily PRE+ value
  • Zero-Inject: the 3ERL instruction to activated Zero-Inject adds a grey area on the graph

In "ACI" mode, which is by default when subscribing to 3ERL only, we need to activate zero-inject when asked by 3ERL. 3ERL is doing some approximation of the final PRE+ price that Enedis will use.

How it works behind the scene:

  1. PRE+: Enedis tracks all the PRE+ values for each 1/4h
  2. PRD3: Enedis also builds up a PRD3 profile for the current day based on sunlight. So we do not know this profile in advance. It is made available the day after in the morning.
  3. Enedis ccomputes for each 1/4h the Adjusted PRE+ for this slot which is: PRE+ x PRD3_Factor
  4. Enedis computes at the end of the day a Daily PRE+ value which is: SUM( All Adjusted PRE+ ) / SUM( All PRD3_Factor )
  5. Enedis will then multiply the Daily PRE+ value to the total excess energy for the day

Since we do not know yet the PRD3 profile of the current day (2), we use the one from a past day which is made public.

From that, we can graph:

  • The PRD3 profile of the day (in red here)
  • The adjusted PRE+ values (in orange here). We can see that the PRE+ values have no impact before 9 AM and after 6 PM here because the PRD3 factors are nearly zero.
  • The Daily Estimation of the final PRE+ (in green). We can see that it is highly impacted during the day but settles at around 2 - 4 PM because adding or removing more data has nearly no impact anymore on the average.

The Daily Estimation of the final PRE+ is the important metric to track.

"ACC" mode (when subscribing to SISOL)

image

This graph displays:

  • PRE+ values for each 1/4h
  • Zero-Inject: the 3ERL instruction to activated Zero-Inject adds a grey area on the graph

In "ACC" mode, this is easier: each 1/4h, the excess energy is multiplied by the PRE+ price for this time slot. So we avoid any excess when prices are going negative and we allow excess when prices are positive.

OpenDTU Intgeration for automatic inverter control

This package can be used with the following integration:

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

Toolbar

image image

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

  • PRD3 Day: the profile of the day in the past to use for the PRD factors. Set to -2 (2 days before) by default because the PRD3 profile for yesterday is only made available late in the morning.

  • Mode: Zero-Inject mode. Mainly used with the OpenDTU Integration

    • Auto: 3ERL API flags are used to control zero-inject
    • On: Zero-Inject is enabled (forced ON)
    • Off: Zero-Inject is disabled (forced OFF)
  • Zero-Inject: status when Zero-Inject is ON

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

Installation

package-3erl.yaml

Make sure you have configured Home Assistant to support packages.

homeassistant:
  packages: !include_dir_named packages

Put the file package-3erl.yaml in the packages folder:

image

Edit package-3erl.yaml according to your needs.

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:

##########
# 3ERL Zero-Inject Instructions and Daily PRE+ Estimate
# Ref: https://gist.github.com/mathieucarbou/8d83d25247821e85a693dea61fe4f0d2
#
# SPDX-License-Identifier: MIT
# Copyright (C) Mathieu Carbou
##########
# https://www.home-assistant.io/integrations/input_number
input_number:
# 🖐️ PRD3 (Sunlight) Profile to use to estimate the daily PRE+ value in ACI (not ACC)
# -1 means yesterday (yesterday curve is only ade available in the morning, so some data will be missed but not a lot)
# -2 means 2 days ago
# -7 means same day last week
# Default: -2
electricite_profil_dynamique_prd3_day:
name: "Électricité: Profil Dynamique PRD3 Jour"
unit_of_measurement: d
min: -7
max: -1
step: 1
mode: slider
# https://www.home-assistant.io/integrations/input_select/
input_select:
inverters_zero_inject_mode:
name: Inverters Zero-Inject mode
options:
- "Auto"
- "On"
- "Off"
# 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: 30
binary_sensor:
- name: "3ERL: Bridage Demandé"
icon: "mdi:car-speed-limiter"
unique_id: 347A6F4D-6B09-4A20-9C2C-D611ED23EF4B
value_template: "{{ value_json['Bridage'] }}"
- name: "3ERL: Bridage CDC Demandé"
icon: "mdi:car-speed-limiter"
unique_id: B6DC9FBF-DDD0-423F-B9A2-A46CF52EC8CA
value_template: "{{ value_json['Bridage_CDC'] }}"
sensor:
- name: "3ERL: Tendance du jour"
icon: "mdi:trending-up"
unique_id: 31F1BED4-8100-44A4-88BA-DB45450611CF
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 %}
# # For 3ERL: Heure_Update is the last time of the time slot
# - name: "Électricité: PREP Timeslot"
# icon: "mdi:clock"
# unique_id: 1D93CA5A-BED5-499D-A164-15609D78657F
# device_class: timestamp
# value_template: >-
# {# value is something like: 03/03/2026 10:45 #}
# {% set value = value_json['Heure_Update'] %}
# {% set y = value[6:10] %}
# {% set m = value[3:5] %}
# {% set d = value[0:2] %}
# {% set hm = value[11:16] %}
# {{ ((y + '-' + m + '-' + d + 'T' + hm) | as_datetime | as_local - timedelta(minutes=15)).isoformat() }}
# json_attributes:
# - Dernier_PREP
# Prix equilibre RTE: https://www.services-rte.com/fr/visualisez-les-donnees-publiees-par-rte/equilibrage.html
- 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é: PREP Timeslot"
icon: "mdi:clock"
unique_id: 1D93CA5A-BED5-499D-A164-15609D78657F
device_class: timestamp
availability: "{{ value_json['values'] and value_json['values']|length > 0 }}"
value_template: "{{ value_json['values'][0]['date'] }}"
json_attributes_path: "$.values[0]"
json_attributes:
- pre
# https://www.home-assistant.io/integrations/command_line
command_line:
# Get the Enedis PRD3 profiling curve applied to the PRE+ prices to get an approximation.
# This PRD3 curve depends only on sunlight, so we better take the closest day available.
# This sensors keeps the sum of all factors from mignight, plus as attributes the last values (last quarter of hour and last factor)
# See: https://data.enedis.fr/pages/coefficients-de-profils-dynamiques-jplus1-contenu/
# See: https://opendata.enedis.fr/datasets/coefficients-de-profils-dynamiques-anticipes-en-j1en-j1
- sensor:
name: "Électricité: Profil Dynamique PRD3"
unique_id: 18F026A2-7F17-4B09-9074-EE5F9D888A12
state_class: measurement
# scan_interval is high on purpose: the sensors are updated on-demande through an automations that
# is only calling the API every 20 sec past the quarter of hour until the update is received
scan_interval: 86400
command: >-
curl
-s
-X POST
-d "action=exports"
-d "output=exportDirect"
-d "format=json"
-d "dataset=koumoul://7okolrt07nor9cv103spkfzc"
-d "apikey=false"
-d "datefield=horodate"
-d "select=horodate, coefficient_dynamique_j_1"
-d "where=(sous_profil='PRD3_BASE') AND horodate >= '{{ (states('sensor.electricite_prep_timeslot')|as_datetime|as_local + timedelta(days = states('input_number.electricite_profil_dynamique_prd3_day')|int(-2))).strftime('%Y-%m-%d') }}T00:00:00' AND horodate <= '{{ (states('sensor.electricite_prep_timeslot')|as_datetime|as_local + timedelta(days = states('input_number.electricite_profil_dynamique_prd3_day')|int(-2))).strftime('%Y-%m-%dT%H:%M') }}:00'"
-d "group="
-d "order=horodate desc"
"https://openservices.enedis.fr/php/opendata.php"
availability: "{{ value_json|length > 0 and value_json | sum(attribute='coefficient_dynamique_j_1') > 0 }}"
value_template: "{{ value_json | sum(attribute='coefficient_dynamique_j_1') }}"
json_attributes_path: "$[0]"
json_attributes:
- horodate
- coefficient_dynamique_j_1
template:
# https://www.home-assistant.io/integrations/template/#binary-sensor
- binary_sensor:
# Determines whether we should limit the inverters
- name: Inverters Zero-Inject
unique_id: "12E68C95-83ED-41EF-86E6-A0295FC2458C"
state: >-
{% set mode = states('input_select.inverters_zero_inject_mode') %}
{% set aci = states('binary_sensor.3erl_bridage_demande') %}
{% set acc = states('binary_sensor.3erl_bridage_cdc_demande') %}
{{ "on" if mode == "On" or (mode == "Auto" and aci == "on") else "off" }}
# https://www.home-assistant.io/integrations/template/#sensor
- sensor:
# The last PRE_ value extracted from REST response
- 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"
availability: "{{ states('sensor.electricite_prep_timeslot') not in ['unavailable', 'unknown'] }}"
state: "{{ state_attr('sensor.electricite_prep_timeslot', 'pre')['positive']|float }}"
attributes:
date: "{{ (states('sensor.electricite_prep_timeslot')|as_datetime|as_local).isoformat() }}"
timeslot: "{{ (states('sensor.electricite_prep_timeslot')|as_datetime|as_local).strftime('%H:%M') }}"
# The PRD3 factor for the current quarter of hour. Used for graph.
- name: "Électricité: Profil Dynamique PRD3 Factor"
unique_id: "D6A61828-CBA3-41CA-A7AF-9E1AF79F6029"
state_class: measurement
availability: "{{ has_value('sensor.electricite_profil_dynamique_prd3') }}"
state: "{{ state_attr('sensor.electricite_profil_dynamique_prd3', 'coefficient_dynamique_j_1')|float }}"
attributes:
date: "{{ (state_attr('sensor.electricite_profil_dynamique_prd3', 'horodate')|as_datetime|as_local).isoformat() }}"
timeslot: "{{ (state_attr('sensor.electricite_profil_dynamique_prd3', 'horodate')|as_datetime|as_local).strftime('%H:%M') }}"
# The PRD3 timeslot for the current quarter of hour.
- name: "Électricité: Profil Dynamique PRD3 Timeslot"
unique_id: "AF7BFE5B-5AA0-42F7-8323-D2166F8EA4E3"
device_class: timestamp
availability: "{{ has_value('sensor.electricite_profil_dynamique_prd3') }}"
state: "{{ state_attr('sensor.electricite_profil_dynamique_prd3', 'horodate') }}"
# Current PRE+ value for the quarter of hour multiplied by the current PRD3 factor for this quarter of hour
- name: "Électricité: PREP by PRD3 Factor"
unique_id: "B0B14D4E-EDEF-4D18-8FE9-F5B087FA364C"
state_class: measurement
unit_of_measurement: "€/MWh"
availability: "{{ has_value('sensor.electricite_prix_de_reglement_des_ecarts_positifs') and has_value('sensor.electricite_profil_dynamique_prd3_factor') }}"
state: >-
{% set last_sensor_value = states('sensor.electricite_prep_by_prd3_factor')|float(0) %}
{% set prep_timeslot = state_attr('sensor.electricite_prix_de_reglement_des_ecarts_positifs', 'timeslot') %}
{% set prep_value = states('sensor.electricite_prix_de_reglement_des_ecarts_positifs')|float %}
{% set prd3_timeslot = state_attr('sensor.electricite_profil_dynamique_prd3_factor', 'timeslot') %}
{% set prd3_value = states('sensor.electricite_profil_dynamique_prd3_factor')|float %}
{{ last_sensor_value if prep_timeslot != prd3_timeslot else prep_value * prd3_value }}
attributes:
date: "{{ state_attr('sensor.electricite_prix_de_reglement_des_ecarts_positifs', 'date') }}"
timeslot: "{{ state_attr('sensor.electricite_prix_de_reglement_des_ecarts_positifs', 'timeslot') }}"
# This is the currently estimated PRE+ ratio for the day based on the selectd PRD3 curve
- name: "Électricité: PREP Daily Estimation"
unique_id: "AA659353-C08D-4C92-943A-2F44933E768D"
state_class: measurement
unit_of_measurement: "€/MWh"
availability: "{{ has_value('sensor.electricite_prep_by_prd3_factor') and has_value('sensor.electricite_prep_by_prd3_daily_running_sum') and has_value('sensor.electricite_profil_dynamique_prd3') and states('sensor.electricite_profil_dynamique_prd3')|float(0) > 0 }}"
state: >-
{# running sum (meter) #}
{% set running_sum = states('sensor.electricite_prep_by_prd3_daily_running_sum')|float(0) %}
{% set running_sum_last_changed = states.sensor.electricite_prep_by_prd3_daily_running_sum.last_changed %}
{# PRD3 sum #}
{% set prd3_sum = states('sensor.electricite_profil_dynamique_prd3')|float %}
{% set prd3_timeslot = (state_attr('sensor.electricite_profil_dynamique_prd3', 'horodate')|as_datetime|as_local).strftime('%H:%M') %}
{% set prd3_sum_last_changed = states.sensor.electricite_profil_dynamique_prd3.last_changed %}
{# PRD3 factor #}
{% set prd3_factor_timeslot = state_attr('sensor.electricite_prep_by_prd3_factor', 'timeslot') %}
{% set prd3_factor_last_changed = states.sensor.electricite_prep_by_prd3_factor.last_changed %}
{% if prd3_timeslot == prd3_factor_timeslot and running_sum_last_changed > prd3_sum_last_changed and running_sum_last_changed > prd3_factor_last_changed %}
{{ running_sum / prd3_sum }}
{% elif has_value('sensor.electricite_prep_daily_estimation') %}
{{ states('sensor.electricite_prep_daily_estimation')|float }}
{% else %}
{{ states('sensor.electricite_prep_daily_estimation') }}
{% endif %}
attributes:
date: "{{ state_attr('sensor.electricite_prep_by_prd3_factor', 'date') }}"
timeslot: "{{ state_attr('sensor.electricite_prep_by_prd3_factor', 'timeslot') }}"
# https://www.home-assistant.io/integrations/utility_meter/
utility_meter:
# This is the running sum during the day of all PRE+ * PRD3 for each quarter of hour
electricite_prep_by_prd3_daily_running_sum:
name: "Électricité: PREP by PRD3 Daily Running Sum"
unique_id: 0EEB33DF-C2C1-4912-BE78-23F3B65D967B
source: sensor.electricite_prep_by_prd3_factor
cycle: daily
net_consumption: true
delta_values: true
# https://www.home-assistant.io/docs/automation/
automation:
# Notification in case zero-inject order is changed
# This is optional and require you to know how to setup HA notifications.
- id: "0000000000040"
alias: "Solar: Notify Zero-Inject State Change"
triggers:
- trigger: state
entity_id:
- binary_sensor.inverters_zero_inject
to: "on"
- trigger: state
entity_id:
- binary_sensor.inverters_zero_inject
from: "on"
to: "off"
- trigger: homeassistant
event: start
conditions: []
actions:
- if:
- condition: state
entity_id: binary_sensor.inverters_zero_inject
state: "on"
then:
- action: notify.whatsapp_mathieu
data:
message: "[SOLAR] Zero-Inject: ON"
- if:
- condition: state
entity_id: binary_sensor.inverters_zero_inject
state: "off"
then:
- action: notify.whatsapp_mathieu
data:
message: "[SOLAR] Zero-Inject: OFF"
- id: "0000000000043"
alias: "Solar: Update PRE+ and PRD3"
triggers:
- event: start
trigger: homeassistant
- trigger: time_pattern
seconds: /20
minutes: /3
conditions: []
actions:
- if:
- condition: template
value_template: "{{ states('sensor.electricite_prep_timeslot') in ['unavailable', 'unknown'] or now() - states('sensor.electricite_prep_timeslot')|as_datetime > timedelta(minutes=30) }}"
then:
- action: homeassistant.update_entity
data:
entity_id:
- sensor.electricite_prep_timeslot
- if:
- condition: template
value_template: "{{ states('sensor.electricite_profil_dynamique_prd3') in ['unavailable', 'unknown'] or now() - state_attr('sensor.electricite_profil_dynamique_prd3', 'horodate')|as_datetime - timedelta(days=2) > timedelta(minutes=30) }}"
then:
- action: homeassistant.update_entity
data:
entity_id:
- sensor.electricite_profil_dynamique_prd3
type: sections
max_columns: 3
title: 3ERL
path: 3erl
icon: mdi:transmission-tower-export
sections:
- type: grid
cards:
- type: horizontal-stack
cards:
- type: custom:plotly-graph
title: Prices (ACI)
refresh_interval: 60s
hours_to_show: 1d
defaults:
entity:
connectgaps: true
layout:
height: 440
yaxis9:
visible: false
fixedrange: true
xaxis:
rangeselector:
"y": 1.4
bgcolor: grey
buttons:
- count: 6
step: hour
- count: 12
step: hour
- count: 1
step: day
- count: 2
step: day
- count: 7
step: day
- count: 30
step: day
- count: 60
step: day
- count: 90
step: day
entities:
- entity: sensor.electricite_prix_de_reglement_des_ecarts_positifs
name: >
$ex "PRE+ (" + (ys[ys.length - 1] * 0.1).toFixed(2) + "
c€/kWh)"
unit_of_measurement: c€/kWh
show_value: false
time_offset: "-18m"
filters:
- multiply: 0.1
- entity: sensor.electricite_prep_by_prd3_factor
name: >
$ex "PRE+ Adjusted (" + (ys[ys.length - 1] * 0.1).toFixed(2) +
" c€/kWh)"
unit_of_measurement: c€/kWh
show_value: false
time_offset: "-18m"
filters:
- multiply: 0.1
- entity: sensor.electricite_prep_daily_estimation
name: >
$ex "PRE+ Daily Est (" + (ys[ys.length - 1] * 0.1).toFixed(2)
+ " c€/kWh)"
unit_of_measurement: c€/kWh
show_value: false
time_offset: "-18m"
filters:
- multiply: 0.1
- entity: sensor.electricite_profil_dynamique_prd3
attribute: coefficient_dynamique_j_1
time_offset: "-18m"
name: >
$ex "PRD3 Profile (" + (meta.horodate ?
meta.horodate.substring(0, 10) : "????") + ")"
- entity: binary_sensor.3erl_bridage_demande
name: Zero-Inject (3ERL API)
showlegend: true
yaxis: y9
filters:
- map_y: "y === 'on' ? 1 : 0"
fill: tozeroy
line:
color: rgba(100,100,100,0.1)
shape: hv
connectgaps: false
width: 0
grid_options:
columns: full
- type: horizontal-stack
cards:
- type: custom:plotly-graph
title: Prices (ACC)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment