|
## |
|
# 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" |