|
########## |
|
# Automatically limit inverters based on grid excess with Home Assistant and OpenDTU |
|
# Ref: https://gist.github.com/mathieucarbou/382556f1279d612962e03232544692d1 |
|
# |
|
# SPDX-License-Identifier: MIT |
|
# Copyright (C) Mathieu Carbou |
|
########## |
|
|
|
# https://www.home-assistant.io/integrations/input_number |
|
input_number: |
|
# 🖐️ Number of inverters |
|
inverters_count: |
|
name: Inverters Count |
|
min: 0 |
|
max: 20 |
|
step: 1 |
|
mode: box |
|
# 🖐️ 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 |
|
# 🖐️ Inverters minimal power, usually 50W, 100W. Has to be more than 10W to keep the iverters workimng |
|
# This should be such that minimalPowerLimit * number of inverters == minimal home consumption |
|
# Default: 50W |
|
inverters_minimal_power: |
|
name: Inverters Minimal Power |
|
unit_of_measurement: W |
|
min: 10 |
|
max: 2000 |
|
step: 1 |
|
mode: box |
|
# Limit sent to inverters. Will be automatically computed but can be manually set also. |
|
inverters_power_limit: |
|
name: Inverters Power Limit |
|
unit_of_measurement: W |
|
min: 0 |
|
max: 2000 |
|
step: 1 |
|
mode: box |
|
# Target excess or import to stay close to |
|
# Will be automatically set based on below automations but can also beset manually |
|
# Default is 0 |
|
inverters_excess_setpoint: |
|
name: Inverters Excess Setpoint |
|
unit_of_measurement: W |
|
min: -10000 |
|
max: 10000 |
|
step: 1 |
|
mode: box |
|
|
|
template: |
|
# https://www.home-assistant.io/integrations/template/#sensor |
|
- sensor: |
|
# Get the maximum voltage of inverters to detect potential risk of production stop |
|
# 🖐️ Put your inverter S/N in this list in lowercase, any order |
|
- name: Max Inverter Voltage |
|
unique_id: 42D1A95B-7A70-46CF-9734-497CDDCE5674 |
|
state_class: measurement |
|
device_class: voltage |
|
unit_of_measurement: V |
|
state: >- |
|
{{ ['1410a01ec916', '1410a01ed6ca', '1410a01ed5a9', '1410a01ed5f0', '1410a01f092d', '1410a01ed604', '1410a01f010c', '1410a01e2cdd']|map('regex_replace', '(.+)', "sensor.inverter_\\1_voltage")|map('states')|map('float', 0)|max }} |
|
|
|
# Get the maximum temeprature of inverters to detect potential risk of production stop |
|
# 🖐️ Put your inverter S/N in this list in lowercase, any order |
|
- name: Max Inverter Temperature |
|
unique_id: 272E179C-3A11-44A1-B674-8C5A603033A0 |
|
state_class: measurement |
|
device_class: temperature |
|
unit_of_measurement: "°C" |
|
state: >- |
|
{{ ['1410a01ec916', '1410a01ed6ca', '1410a01ed5a9', '1410a01ed5f0', '1410a01f092d', '1410a01ed604', '1410a01f010c', '1410a01e2cdd']|map('regex_replace', '(.+)', "sensor.inverter_\\1_temperature")|map('states')|map('float', 0)|max }} |
|
|
|
# This sensor computes your inverters energy and makes sure it does not reset to 0 |
|
# 🖐️ Put your OpenDTU YieldTotal sensor or Shelly sensors |
|
- name: Inverters Energy |
|
unique_id: "011F19F7-E337-488E-B52E-3CC75CBD816A" |
|
state_class: total_increasing |
|
device_class: energy |
|
unit_of_measurement: kWh |
|
# CHOICE #1: Use Shelly (could miss data if Shelly's are down - but more reliable) |
|
state: >- |
|
{% set c = states('sensor.inverters_energy')|float(0) %} |
|
{% set gA = states('sensor.solar_plant_group_a_total_active_energy')|float(0) %} |
|
{% set gB = states('sensor.solar_plant_group_b_total_active_energy')|float(0) %} |
|
{{ [c, gA + gB] | max }} |
|
# CHOICE #2: Use energy counters stored in inverters from OpenDTU (could miss data if inverters are unreachable - which can sometimes happen while the yare producing) |
|
# state: >- |
|
# {% set c = states('sensor.inverters_energy')|float(0) %} |
|
# {% set e = states('sensor.opendtu_ff4930_yield_total')|float(0) %} |
|
# {{ [c, e] | max }} |
|
|
|
# Total Solar Production Power |
|
# 🖐️ 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 |
|
availability: "{{ has_value('sensor.solar_plant_group_a_power') and has_value('sensor.solar_plant_group_b_power') }}" |
|
state: "{{ states('sensor.solar_plant_group_a_power')|float + states('sensor.solar_plant_group_b_power')|float }}" |
|
|
|
# Home Consumed Power: power consumed by your house == the produced power plus the measured grid power |
|
# 🖐️ 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: "{{ has_value('sensor.solar_production_power') and has_value('sensor.grid_power') }}" |
|
state: "{{ states('sensor.solar_production_power')|float + states('sensor.grid_power')|float }}" |
|
|
|
# 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: "{{ has_value('sensor.solar_production_power') and has_value('sensor.grid_power') }}" |
|
state: "{{ [states('sensor.solar_production_power')|float + [states('sensor.grid_power')|float, 0] | min, 0] | max }}" |
|
|
|
# Daily energy used from solar production (C = P - E) |
|
- name: Solar Consumed Energy Meter Daily |
|
unique_id: "01e37eed-3045-4eee-a786-1249567fe305" |
|
state_class: total_increasing |
|
device_class: energy |
|
unit_of_measurement: kWh |
|
availability: "{{ has_value('sensor.inverters_energy_meter_daily') and has_value('sensor.grid_energy_returned_meter_daily') }}" |
|
state: >- |
|
{% set p = states('sensor.inverters_energy_meter_daily')|float(0) %} |
|
{% set e = states('sensor.grid_energy_returned_meter_daily')|float(0) %} |
|
{% set c = states('sensor.solar_consumed_energy_meter_daily')|float(0) %} |
|
{{ [0 if p == 0 or p < c or p < e else c, [0, p - e] | max] | max }} |
|
|
|
# Daily energy consumed by home (H = P + I - E) |
|
- name: Home Consumed Energy Meter Daily |
|
unique_id: A03E558E-DF44-4DAB-852D-0847287DEDDB |
|
state_class: total_increasing |
|
device_class: energy |
|
unit_of_measurement: kWh |
|
availability: "{{ has_value('sensor.inverters_energy_meter_daily') and has_value('sensor.grid_energy_meter_daily') and has_value('sensor.grid_energy_returned_meter_daily') }}" |
|
state: >- |
|
{% set p = states('sensor.inverters_energy_meter_daily')|float(0) %} |
|
{% set i = states('sensor.grid_energy_meter_daily')|float(0) %} |
|
{% set e = states('sensor.grid_energy_returned_meter_daily')|float(0) %} |
|
{% set h = states('sensor.home_consumed_energy_meter_daily')|float(0) %} |
|
{{ [0, p + i - e] | max if p == 0 or i == 0 or p < e else [h, p + i - e] | max }} |
|
|
|
# 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 |
|
# 🖐️ 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 |
|
cycle: daily |
|
# Inverters Energy Meter Daily |
|
inverters_energy_meter_daily: |
|
name: Inverters Energy Meter Daily |
|
unique_id: 4C4D8D06-C9D2-4408-B21A-1274A6E0F041 |
|
source: sensor.inverters_energy |
|
cycle: daily |
|
|
|
# https://www.home-assistant.io/docs/automation/ |
|
automation: |
|
# Automatically propagate changes in power limit input number to inverters |
|
- 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 = states('input_number.inverters_minimal_power')|int(50) %} |
|
{% 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 |
|
# 🖐️ set your Grid Power sensor for `sensor.grid_power` |
|
# 🖐️ Update the list of entity IDs matching your inverters S/N: these sensors come from OpenDTU |
|
- id: "0000000000039" |
|
alias: "Solar: Auto update power limits" |
|
trigger: |
|
- trigger: time_pattern |
|
#minutes: /1 |
|
seconds: /30 |
|
condition: |
|
- condition: state |
|
entity_id: binary_sensor.opendtu_ff4930_status |
|
state: "on" |
|
action: |
|
- action: input_number.set_value |
|
data: |
|
value: >- |
|
{% set minimalPowerLimit = states('input_number.inverters_minimal_power')|int(50) %} |
|
{% 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 count = states('input_number.inverters_count')|int(0) %} |
|
{% set missedPower = (grid - setpoint) / count if count > 0 else nominalPower %} |
|
{{ [nominalPower, [minimalPowerLimit, powerLimit + missedPower|round(0, "ceil")]|max]|min|round }} |
|
target: |
|
entity_id: |
|
- input_number.inverters_power_limit |
|
|
|
# Update the setpoint depending on events |
|
# 🖐️ 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. |
|
# 🖐️ 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.inverters_zero_inject |
|
- 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.inverters_zero_inject |
|
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.inverters_zero_inject |
|
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 |
L'intégration utilise le paramètre non persistent. Voir le code.
par contre certaines version du firmware hoymiles ont un bug et mettent les limites persistantes. Il faut voir avec le support pour une MAJ.