1
0
Fork 0
ha-config/config/packages/optimise_solar.yaml

381 lines
16 KiB
YAML

automation:
# The inverter can only generate 5kW of AC power. IFF the battery is not-full, it can generate 5kW of AC _and_ charge
# battery at up to 6.6kW, which saturates our 8.46 kW of PV
#
# So, preventing the battery from getting full too early on a sunny day will increase our overall yield, because the
# inverter won't have to derate the PV power to 5kW.
#
# [1]: https://discord.com/channels/936031869001158666/1008992991643455529
# [2]: https://discord.com/channels/936031869001158666/936031869001158669/1011882768021590036
- id: 05bdc8eb58714c26c2fe
alias: Inverter - maximise output
mode: restart
trigger:
- platform: state
entity_id:
- sensor.inverter_pv_power
- sensor.inverter_battery_level
- sensor.inverter_active_power
- sensor.home_weather_cloud_coverage
- sensor.home_weather_forecast_cloud_coverage
- sensor.solcast_forecast_today
- sensor.solcast_forecast_this_hour
- sensor.solcast_forecast_remaining_today
# - sensor.home_weather_forecast_condition
- sensor.home_weather_condition
# - weather.home
- weather.home_hourly # can use attributes on this one to make decisions about the coming hours
# - weather.home_weather
- sun.sun # use `next_setting` attribute to ensure battery is online at least an hour before sunset
not_to:
- unavailable
- unknown
variables:
# Magic numbers
ems_self_consume: 0
ems_forced: 2
battery_charge: 0xAA
battery_discharge: 0xBB
battery_stop: 0xCC
active_power_limit: 4999 # W
active_power_buffer: 400 # W - how much below limit we want to sit
battery_upper_limit: 99.5 # % - above this, let the BMS choose the charge rate
battery_capacity: 12.8 # kWh
# Shorthands
current_active_power: "{{ states('sensor.inverter_active_power') | int(default=active_power_limit) }}"
current_pv_power: "{{ states('sensor.inverter_pv_power') | int(default=0) }}"
current_load_power: "{{ states('sensor.inverter_load_power') | int(default=current_pv_power) }}"
current_ems_mode: "{{ states('sensor.inverter_ems_mode_raw') | int(default=-1) }}"
current_battery_mode: "{{ states('sensor.inverter_forced_battery_mode_raw') | int(default=0) }}"
battery_level: "{{ states('sensor.inverter_battery_level') | float(default=100) }}"
forced_battery_power: "{{ states('sensor.inverter_battery_forced_charge_discharge_power') | int(default=0) }}"
forecast_total: "{{ states('sensor.solcast_forecast_today') | float(default=0) }}"
forecast_remaining: "{{ states('sensor.solcast_forecast_remaining_today') | float(default=0) }}"
forecast_hour: "{{ states('sensor.solcast_forecast_this_hour') | float(default=0) / 1000.0 }}"
forecast_remaining_pessimistic: "{{ [0, forecast_remaining - forecast_hour] | max }}"
# battery_lower_limit: >
# {% if is_state('sensor.home_weather_condition', 'sunny') %}
# {{ 10 }}
# {% elif is_state('sensor.home_weather_condition', 'partlycloudy') %}
# {{ 20 }}
# {% else %}
# {{ 70 }}
# {% endif %}
battery_lower_limit: 10 # ignoring above as weather forecast is too unreliable anyway
target_active_power: "{{ active_power_limit - active_power_buffer }}"
# TODO: make this scale proportionally to how far it is from target battery charge.
desired_forced_battery_power: >
{% if current_pv_power > target_active_power %}
{{ current_pv_power - target_active_power }}
{% else %}
10
{% endif %}
is_forced_charging: >
{{ current_ems_mode == ems_forced and current_battery_mode == battery_charge }}
is_forced_discharging: >
{{ current_ems_mode == ems_forced and current_battery_mode == battery_discharge }}
is_self_consuming: >
{{ current_ems_mode == ems_self_consume }}
kwh_until_full: >
{{ battery_capacity * ((100 - battery_level)/100) }}
enough_in_day: >
{{ 2.5 * kwh_until_full < forecast_remaining_pessimistic }}
generating_more_than_usage: >
{{ current_pv_power > (1.5 * current_load_power) }}
# target_battery_level: >
# {{ [[battery_lower_limit, 100.0 * (1.0 - (forecast_remaining_pessimistic/forecast_total)) | round(2)] | max, 100] | min }}
target_battery_level: >
{% if enough_in_day %}
{{ [battery_level - 10, states('input_number.inverter_battery_reserve') | int + 5] | max }}
{% else %}
{{ [battery_level, states('input_number.inverter_battery_reserve') | int + 5] | max }}
{% endif %}
battery_high_enough: "{{ battery_level >= target_battery_level }}"
battery_too_high: "{{ battery_level >= battery_upper_limit }}"
sunsetting: >
{{ now() + timedelta(hours = 2) > state_attr('sun.sun', 'next_setting') | as_datetime }}
should_slow_battery: >
{{
not sunsetting
and enough_in_day
and generating_more_than_usage
and battery_high_enough
and not battery_too_high
}}
should_discharge_battery: >
{{ should_slow_battery
and current_pv_power < (target_active_power - 500)
and battery_level < 92
}}
target_discharge_power: >
{{ [10, ((target_active_power - 500) - current_pv_power) | int] | max }}
action:
# TODO: discharge battery if too high and PV has dropped while forecast remains high
- choose:
- conditions:
- "{{ should_discharge_battery }}"
sequence:
- service: input_number.set_value
target:
entity_id: input_number.inverter_forced_mode_battery_power
data_template:
value: "{{ target_discharge_power }}"
- condition: "{{ not is_forced_discharging }}"
- service: script.inverter_force_battery_discharge
- conditions:
- "{{ should_slow_battery }}"
sequence:
- service: input_number.set_value
target:
entity_id: input_number.inverter_forced_mode_battery_power
data_template:
value: "{{ desired_forced_battery_power }}"
- condition: "{{ not is_forced_charging }}"
- service: script.inverter_force_battery_charge
- conditions:
- not:
- "{{ is_self_consuming }}"
sequence:
- service: script.inverter_self_consumption
default: []
- id: d5fa94e6-772a-4903-882a-4ed8cfd7854e
alias: Inverter - maximise output (new)
mode: restart
trigger:
- platform: state
entity_id:
- sensor.inverter_pv_power
- sensor.inverter_battery_level
- sensor.inverter_active_power
- sensor.home_weather_cloud_coverage
- sensor.home_weather_forecast_cloud_coverage
- sensor.solcast_forecast_today
- sensor.solcast_forecast_this_hour
- sensor.solcast_forecast_remaining_today
# - sensor.home_weather_forecast_condition
- sensor.home_weather_condition
# - weather.home
- weather.home_hourly # can use attributes on this one to make decisions about the coming hours
# - weather.home_weather
- sun.sun # use `next_setting` attribute to ensure battery is online at least an hour before sunset
not_to:
- unavailable
- unknown
variables:
# Magic numbers
ems_self_consume: 0
ems_forced: 2
battery_charge: 0xAA
battery_discharge: 0xBB
battery_stop: 0xCC
active_power_limit: 4999 # W
active_power_buffer: 400 # W - how much below limit we want to sit
battery_upper_limit: 99.5 # % - above this, let the BMS choose the charge rate
battery_capacity: 12.8 # kWh
# Shorthands
current_active_power: "{{ states('sensor.inverter_active_power') | int(default=active_power_limit) }}"
current_pv_power: "{{ states('sensor.inverter_pv_power') | int(default=0) }}"
current_load_power: "{{ states('sensor.inverter_load_power') | int(default=current_pv_power) }}"
current_ems_mode: "{{ states('sensor.inverter_ems_mode_raw') | int(default=-1) }}"
current_battery_mode: "{{ states('sensor.inverter_forced_battery_mode_raw') | int(default=0) }}"
battery_level: "{{ states('sensor.inverter_battery_level') | float(default=100) }}"
forced_battery_power: "{{ states('sensor.inverter_battery_forced_charge_discharge_power') | int(default=0) }}"
forecast_total: "{{ states('sensor.solcast_forecast_today') | float(default=0) }}"
forecast_remaining: "{{ states('sensor.solcast_forecast_remaining_today') | float(default=0) }}"
forecast_hour: "{{ states('sensor.solcast_forecast_this_hour') | float(default=0) / 1000.0 }}"
forecast_remaining_pessimistic: "{{ [0, forecast_remaining - forecast_hour] | max }}"
# battery_lower_limit: >
# {% if is_state('sensor.home_weather_condition', 'sunny') %}
# {{ 10 }}
# {% elif is_state('sensor.home_weather_condition', 'partlycloudy') %}
# {{ 20 }}
# {% else %}
# {{ 70 }}
# {% endif %}
battery_lower_limit: 10 # ignoring above as weather forecast is too unreliable anyway
target_active_power: "{{ active_power_limit - active_power_buffer }}"
# TODO: make this scale proportionally to how far it is from target battery charge.
desired_forced_battery_power: >
{% if current_pv_power > target_active_power %}
{{ current_pv_power - target_active_power }}
{% else %}
10
{% endif %}
is_forced_charging: >
{{ current_ems_mode == ems_forced and current_battery_mode == battery_charge }}
is_forced_discharging: >
{{ current_ems_mode == ems_forced and current_battery_mode == battery_discharge }}
is_self_consuming: >
{{ current_ems_mode == ems_self_consume }}
kwh_until_full: >
{{ battery_capacity * ((100 - battery_level)/100) }}
enough_in_day: >
{{ 2.5 * kwh_until_full < forecast_remaining_pessimistic }}
generating_more_than_usage: >
{{ current_pv_power > (1.5 * current_load_power) }}
# target_battery_level: >
# {{ [[battery_lower_limit, 100.0 * (1.0 - (forecast_remaining_pessimistic/forecast_total)) | round(2)] | max, 100] | min }}
target_battery_level: >
{% if enough_in_day %}
{{ battery_level - 10 }}
{% else %}
{{ battery_level }}
{% endif %}
battery_high_enough: "{{ battery_level >= target_battery_level }}"
battery_too_high: "{{ battery_level >= battery_upper_limit }}"
sunsetting: >
{{ now() + timedelta(hours = 2) > state_attr('sun.sun', 'next_setting') | as_datetime }}
should_slow_battery: >
{{
not sunsetting
and enough_in_day
and generating_more_than_usage
and battery_high_enough
and not battery_too_high
}}
target_discharge_power: >
{% if (battery_level - target_battery_level) > 1 %}
{{ [6000, (battery_level - target_battery_level) * 1000 | int] | min }}
{% else %}
100
{% endif %}
action:
# TODO: discharge battery if too high and PV has dropped while forecast remains high
- choose:
- conditions:
- "{{ should_slow_battery }}"
sequence:
- service: input_number.set_value
target:
entity_id: input_number.inverter_forced_mode_battery_power
data_template:
value: "{{ target_discharge_power }}"
- condition: "{{ not is_forced_discharging }}"
- service: script.inverter_force_battery_discharge
- conditions:
- not:
- "{{ is_self_consuming }}"
sequence:
- service: script.inverter_self_consumption
default: []
template:
- sensor:
- unique_id: e1152f05-57d8-4821-b8fc-d6bca771a3b5
name: Inverter target active power
unit_of_measurement: W
state_class: measurement
device_class: power
attributes:
solar: "true"
state: >-
{{ 4999 - states('input_number.inverter_active_power_buffer') | int(default=400) }}
- unique_id: a59d08fc-92bb-47c6-b015-8ed0e475f2e5
name: Inverter desired forced battery power
unit_of_measurement: W
state_class: measurement
device_class: power
attributes:
solar: "true"
state: >-
{% set current_pv_power = states('sensor.inverter_pv_power') | int(default=0) %}
{% set target_active_power = states('sensor.inverter_target_active_power') | int %}
{% if current_pv_power > target_active_power %}
{{ current_pv_power - target_active_power }}
{% else %}
10
{% endif %}
- unique_id: d257272c-3ac0-4d93-9ef7-00717757cef3
name: Solar forecast remaining pessimistic
device_class: energy
unit_of_measurement: kWh
attributes:
solar: "true"
state: >-
{% set forecast_remaining = states('sensor.solcast_forecast_remaining_today') | float(default=0) %}
{% set forecast_hour = states('sensor.solcast_forecast_this_hour') | float(default=0) / 1000.0 %}
{{ [0, forecast_remaining - forecast_hour] | max }}
- unique_id: dfdaea61-8c96-4b09-82bf-b76e25800cca
name: Solar future peak amount
device_class: energy
unit_of_measurement: kWh
attributes:
solar: "true"
period_start: >-
{{ (state_attr('sensor.solcast_forecast_today', 'detailedForecast')
| selectattr('period_start', 'ge', now())
| max(attribute='pv_estimate90')).period_start }}
state: >-
{{ (state_attr('sensor.solcast_forecast_today', 'detailedForecast')
| selectattr('period_start', 'ge', now())
| max(attribute='pv_estimate90')).pv_estimate90 }}
- unique_id: 6bf7ad20-cf6a-4689-8214-13cd63de80a9
name: Inverter target battery level
state_class: measurement
unit_of_measurement: "%"
attributes:
solar: "true"
state: >-
{% set battery_lower_limit = states('input_number.inverter_battery_reserve') | int + 5 %}
{% set forecast_remaining_pessimistic = states('sensor.solar_forecast_remaining_pessimistic') | float(default=0) %}
{% set forecast_total = states('sensor.solcast_forecast_today') | float(default=0) %}
{% if states('sensor.solar_future_peak_amount') | float(default=0) <= 1.5 %}
100
{% elif states('sensor.solar_future_peak_amount') | float(default=0) <= 2.5 %}
90
{% else %}
{{ [[battery_lower_limit, 100.0 * (1.0 - (forecast_remaining_pessimistic/forecast_total)) | round(2)] | max, 100] | min }}
{% endif %}
- unique_id: f603d8f8-92ce-4e77-b271-048110394658
name: Inverter battery mode
attributes:
solar: "true"
icon: >-
{% if this.state == 'self-consumption' %}
mdi:battery-sync
{% elif this.state == 'forced charging' %}
mdi:battery-arrow-down
{% elif this.state == 'forced discharging' %}
mdi:battery-arrow-up
{% elif this.state == 'forced stop' %}
mdi:battery-off
{% endif %}
state: >-
{% set ems = states('sensor.inverter_ems_mode_raw') | int(default=-1) %}
{% set mode = states('sensor.inverter_forced_battery_mode_raw') | int(default=0) %}
{% if ems == 0 %}
self-consumption
{% elif ems == 2 %}
{% if mode == 0xAA %}
forced charging
{% elif mode == 0xBB %}
forced discharging
{% elif mode == 0xCC %}
forced stop
{% else %}
unknown
{% endif %}
{% else %}
unknown
{% endif %}
input_number:
inverter_active_power_buffer:
name: Inverter active power buffer
min: 100
max: 1000
step: 1
unit_of_measurement: W
mode: box