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