Thank you, I have a similar installation plus an ev charger. Will try to set this up and test it !
Itβs probably so specific to my particular situation that it will be of little value to anyone else.
Can you show us the other tabs for your energy monitoring please. Iβm intriqued
For energy monitoring I just have a screen full of multiple entity cards showing power, energy today, energy this month, and percentages of the total for each. The energy totals are from an Emporia Vue and a Shelly EM. I also have power flow and energy flow dashboards but I never look at them. Theyβre just the standard cards.
The more interesting dashboards (and automations) have to do with importing grid power. I monitor grid voltage and adjust how much power I am importing. This is necessary because our grid is incredibly fragile. Itβs very easy to cause the voltage to drop so much that it causes problems.
Hi, I have been working on something very similar to your system. Dynamic level of battery charging and dynamic charge rate so any charging is less stressfull on the battery.
This automation manages battery charging during night rate electricity periods by dynamically calculating the required charge power and target state-of-charge (SOC) based on the solar forecast and time remaining until day rate.
Key features:
- Uses Solcast solar forecasts to set different SOC targets (e.g. charge less if lots of solar is forecast for tomorrow, charge more if little is forecast).
- Adjusts force charge power dynamically to ensure the target SOC is reached just before the end of the cheap night rate.
- Switches inverter modes automatically between Force Charge and Feed-in First depending on the tarrif rates.
- Sends email notifications with detailed status, forecasts, SOC targets, and inverter readings at key moments (start, update, end, or target reached).
- Handles edge cases (e.g. backup mode, SOC already above target).
This is version v5 of the automation, de-personalised for sharing. Replace placeholders such as [email protected] and notify.your_email_service with your own details.
My system comprises of the following:-
FoxESS 5kWh Inverter
20.72kWh of Fox battery
17 Solar panels (9 North facing and 8 soth facing)
Modbus integration to HA from the inverter
Solcast solar forcast updated 4 times a day
EON EV charger electricity tarrif that gives 7 hours cheap electricity overnight (night rate is 6.7pKwh)
I receive SEG payments of 16.5pKwh for export
I charge my batteries at night and export all the solar I can during the day after any house use.
I use a calendar in HA to store the tarrif day and night rate times
alias: Battery Charging - Dynamic Night Rate Charge Power v5
description: >
Automates battery charging during night rate periods based on solar forecast
and current SOC, using next Day rate from calendar.get_events.
# βββββββββββββββββββββββββββββββββββββββ
# TRIGGERS - When should this automation run?
# βββββββββββββββββββββββββββββββββββββββ
triggers:
# Trigger when Solcast solar forecast is updated
- id: solcast_update
entity_id: sensor.solcast_pv_forecast_api_last_polled
trigger: state
# Trigger at the END of the Night rate (switch to Feed-in First mode)
- id: night_rate_end
entity_id: sensor.electricity_current_rate
from: Night rate
trigger: state
# Trigger at the START of the Night rate (start Force Charge mode)
- id: night_rate_start
entity_id: sensor.electricity_current_rate
to: Night rate
trigger: state
# Trigger if inverter unexpectedly enters "Back-up" mode (after 1 min)
- id: backup_work_mode
trigger: state
entity_id:
- select.foxinvertermodbus_work_mode
to: Back-up
for:
minutes: 1
# Trigger whenever Battery SOC changes
- id: soc_reached
trigger: state
entity_id: sensor.foxinvertermodbus_battery_soc
conditions: [] # No global conditions
# βββββββββββββββββββββββββββββββββββββββ
# ACTIONS - What should happen when triggered?
# βββββββββββββββββββββββββββββββββββββββ
actions:
# 1. Get calendar events for the next 2 days (to find next Day rate start)
- action: calendar.get_events
target:
entity_id: calendar.electricity_prices_daily
data:
start_date_time: "{{ now().isoformat() }}"
end_date_time: "{{ (now() + timedelta(days=2)).isoformat() }}"
response_variable: events_data
# 2. Define variables for SOC targets, charging power calculations, etc.
- variables:
# Mapping forecast ranges (kWh) β target SOC %
soc_levels:
- min: 0
max: 7
soc: 100
- min: 7.001
max: 11
soc: 96
- min: 11.001
max: 15
soc: 92
- min: 15.001
max: 18
soc: 88
- min: 18.001
max: 21
soc: 84
- min: 21.001
max: 25
soc: 80
- min: 25.001
max: 28
soc: 76
- min: 28.001
max: 30
soc: 72
- min: 30.001
max: 68
soc: 70
# Min/max allowed charge power (kW)
min_power_kw: 0.1
max_power_kw: 5
# Efficiency uplift factor (accounts for losses)
efficiency_uplift: 1.3
# Inputs from sensors
forecast_today: "{{ states('sensor.solcast_pv_forecast_forecast_today') | float(0) }}"
current_soc: "{{ states('sensor.foxinvertermodbus_battery_soc') | float(0) }}"
total_capacity_kwh: 20.72
now_time: "{{ now() }}"
# Desired SOC (Force Charge mode)
desired_soc: >
{% set forecast = forecast_today %}
{% set soc = namespace(val=100) %}
{% for level in soc_levels %}
{% if forecast >= level.min and forecast <= level.max %}
{% set soc.val = level.soc %}
{% endif %}
{% endfor %}
{{ soc.val }}
# Desired SOC (after Night rate ends / normal use)
desired_soc_self_use: >
{% set forecast = forecast_today %}
{% set soc = namespace(val=100) %}
{% for level in soc_levels %}
{% if forecast >= level.min and forecast <= level.max %}
{% set soc.val = level.soc %}
{% endif %}
{% endfor %}
{{ soc.val }}
# Next "Day rate" time (from calendar) or default 06:01
target_time: >
{% set cal_events = events_data['calendar.electricity_prices_daily'].events %}
{% set matches = cal_events | selectattr('summary', 'search', '(?i)^Day rate') | list %}
{% set next_event = matches[0] if matches else None %}
{% if next_event %}
{{ as_datetime(next_event.start) }}
{% else %}
{{ now().replace(hour=6, minute=1, second=0, microsecond=0) }}
{% endif %}
# Ensure target time is always in the future
adjusted_target_time: >
{% set now_dt = now() %}
{% set target_dt = as_datetime(target_time) %}
{% if now_dt > target_dt %}
{{ target_dt + timedelta(days=1) }}
{% else %}
{{ target_dt }}
{% endif %}
# Time remaining (hours) until target
time_remaining_hours: >
{% set now_dt = now() %}
{% set adjusted = as_datetime(adjusted_target_time) %}
{{ ((adjusted - now_dt).total_seconds() / 3600) | round(2) }}
# SOC difference to reach
soc_delta: "{{ [desired_soc - current_soc, 0] | max }}"
# Energy needed (kWh) to reach desired SOC
energy_needed_kwh: "{{ ((soc_delta / 100) * total_capacity_kwh) | round(2) }}"
# Required average charge power (kW) within time remaining
required_power_kw: |
{% if time_remaining_hours > 0 %}
{% set raw_power = ((energy_needed_kwh * efficiency_uplift) / time_remaining_hours) | round(2) %}
{% else %}
{% set raw_power = 0 %}
{% endif %}
{{ [max_power_kw, [min_power_kw, raw_power] | max] | min }}
# 3. Email configuration (replace with your own notify service)
- variables:
email_target: [email protected]
email_action: notify.your_email_service
email_message: >-
Timestamp (UK): {{ now().strftime('%d/%m/%Y %H:%M:%S') }}<br><br>
Solcast forecast: {{ forecast_today }} kWh<br>
Current SOC: {{ current_soc }}%<br>
Desired SOC for Force Charge: {{ desired_soc }}%<br>
Desired SOC after Night (Feed-in First): {{ desired_soc_self_use }}%<br>
Inverter Work Mode: {{ states('select.foxinvertermodbus_work_mode') }}<br>
Battery Temperature: {{ states('sensor.foxinvertermodbus_battery_temp') }}Β°C<br>
BMS Cell mV Low: {{ states('sensor.foxinvertermodbus_bms_cell_mv_low') }}mV<br>
BMS Cell mV High: {{ states('sensor.foxinvertermodbus_bms_cell_mv_high') }}mV<br>
Battery Voltage: {{ states('sensor.foxinvertermodbus_batvolt') }}V<br>
Battery Current: {{ states('sensor.foxinvertermodbus_bat_current') }}A<br>
Max Charge Current: {{ states('sensor.foxinvertermodbus_max_charge_current') }}A<br>
Max Discharge Current: {{ states('sensor.foxinvertermodbus_max_discharge_current') }}A<br>
Charge Power: {{ states('sensor.foxinvertermodbus_battery_charge') }}W<br>
Discharge Power: {{ states('sensor.foxinvertermodbus_battery_discharge') }}W<br><br>
Total capacity: {{ total_capacity_kwh }} kWh<br>
Target time: {{ as_datetime(target_time).strftime('%A, %d %B %Y %H:%M:%S') }}<br>
Adjusted target time: {{ as_datetime(adjusted_target_time).strftime('%A, %d %B %Y %H:%M:%S') }}<br>
Time remaining hours: {{ time_remaining_hours }}<br>
SOC delta: {{ soc_delta }}%<br>
Energy needed: {{ energy_needed_kwh }} kWh<br>
Required power: {{ required_power_kw }} kW<br>
Current Force Charge Power: {{ states('number.foxinvertermodbus_force_charge_power') | float }} kW<br>
Min Power Limit: {{ min_power_kw }} kW<br>
Max Power Limit: {{ max_power_kw }} kW<br>
Efficiency uplift factor: {{ efficiency_uplift }}<br>
email_title_start: >
Battery Force STARTED Charging to {{ desired_soc }}% at {{ required_power_kw }}kW
email_title_solcast_update: >
Battery Max SOC changed to {{ desired_soc_self_use }}% at {{ required_power_kw }}kW
email_title_end: >
Battery Max SOC changed to {{ desired_soc_self_use }}% at {{ required_power_kw }}kW
# 4. Choose block - actions depend on the trigger
- choose:
# CASE A: Night rate starts β begin Force Charge
- conditions:
- condition: trigger
id: night_rate_start
- condition: state
entity_id: sensor.electricity_current_rate
state: Night rate
sequence:
- action: number.set_value
target:
entity_id: number.foxinvertermodbus_max_soc
data:
value: "{{ current_soc }}" # Prevents instant stop
- delay: 5
- action: select.select_option
target:
entity_id: select.foxinvertermodbus_work_mode
data:
option: Force Charge
- action: "{{ email_action }}"
data:
title: "{{ email_title_start }}"
message: "{{ email_message }}"
target: "{{ email_target }}"
# CASE B: Solcast update or backup mode β update force charge parameters
- conditions:
- condition: trigger
id:
- solcast_update
- backup_work_mode
- condition: state
entity_id: sensor.electricity_current_rate
state: Night rate
- condition: template
value_template: "{{ required_power_kw > 0 }}"
sequence:
- action: number.set_value
target:
entity_id: number.foxinvertermodbus_force_charge_power
data:
value: "{{ required_power_kw }}"
- delay: 5
- action: number.set_value
target:
entity_id: number.foxinvertermodbus_max_soc
data:
value: "{{ desired_soc }}"
- action: select.select_option
target:
entity_id: select.foxinvertermodbus_work_mode
data:
option: Force Charge
- action: "{{ email_action }}"
data:
title: "{{ email_title_solcast_update }}"
message: "{{ email_message }}"
target: "{{ email_target }}"
# CASE C: Night rate ends β switch back to Feed-in First
- conditions:
- condition: trigger
id: night_rate_end
sequence:
- action: select.select_option
target:
entity_id: select.foxinvertermodbus_work_mode
data:
option: Feed-in First
- action: "{{ email_action }}"
data:
title: "{{ email_title_end }}"
message: "{{ email_message }}"
target: "{{ email_target }}"
# CASE D: SOC reaches the desired target
- conditions:
- condition: trigger
id: soc_reached
- condition: template
value_template: >
{% set prev = trigger.from_state.state | int(0) %}
{% set new = trigger.to_state.state | int(0) %}
{{ new == desired_soc | int(0) and prev == (desired_soc | int(0) - 1) }}
sequence:
- action: "{{ email_action }}"
data:
title: Battery SOC reached {{ desired_soc }}%
message: "{{ email_message }}"
target: "{{ email_target }}"
# DEFAULT CASE: If desired SOC < current SOC β no charging required
default:
- condition: template
value_template: "{{ desired_soc < current_soc }}"
- action: "{{ email_action }}"
data:
title: Desired SOC less than current SOC
message: "{{ email_message }}"
target: "{{ email_target }}"
# βββββββββββββββββββββββββββββββββββββββ
# MODE - Prevent overlapping runs
# βββββββββββββββββββββββββββββββββββββββ
mode: single
Awesome! Thanks for sharing. Iβm testing this right now, with some small adaptations to run without calendar as my tariff is the same everyday and Iβm using a victron
@haysdb this is what I have been looking for - thank you
Granted I do have a way different setup but dynamically changing the battery charge rate is what I want. I donβt ever charge my batteries from the grid though and can export around 60kWh on a good day, so will need to tweak it some for my situation but thank you.
Hi @haysdb fantastic job! Thank you for having shared your sensors code. Would it be also possible for you to do the same with your Adaptative Charging dashboard? I was looking for something like this and it will save me a lot of time if I can get yours. Thanks in advance!
This is great, David. Your ideas and solutions -which you very well explained- help us to do something similar in our home and improve tremendously our energy management and yield.
Wow its really nice to see how others are doing this, while iβm also builing (with the help of ChatGPT) since november my automation.
So far iβm satisfied - i really use a lot of data collected from βinsideβ and βoutsideβ and for weeks my v3 is working quit stable.
Mine is not only based on forecast, i also implemented different strategys with charging rates based on how confident the system thinks it is that we will reach the target by the end of the day based on azimut and some lux-sensor.
Iβll just share my code and would be happy to share opinions about.
alias: PV Batterie Ladung dynamisch V3
description: ""
triggers:
- minutes: /5
trigger: time_pattern
conditions:
- condition: template
value_template: |
{{ now_ts < deadline_ts }}
- condition: template
value_template: |
{{ delta_w >= 200 and seconds_since_change >= 600 }}
actions:
- target:
entity_id: input_number.charging_power
data:
value: "{{ adjusted_target_limit_w | round(0) }}"
action: input_number.set_value
- target:
entity_id: input_datetime.pv_charge_last_change
data:
datetime: "{{ now().strftime('%Y-%m-%d %H:%M:%S') }}"
action: input_datetime.set_datetime
mode: single
variables:
pv_w: "{{ states('sensor.erzeugter_strom') | float(0) }}"
house_w: "{{ states('sensor.hausverbrauch') | float(0) }}"
import_w: "{{ states('sensor.netzbezug') | float(0) }}"
export_w: "{{ states('sensor.netzeinspeisung') | float(0) }}"
lux: "{{ states('sensor.beleuchtungssensor_wohnzimmer_illuminance') | float(0) }}"
missing_kwh: "{{ states('sensor.batterie_fehlende_energie_bis_100') | float(0) }}"
net_rest_kwh: "{{ states('sensor.pv_effektiver_rest_fur_akku') | float(0) }}"
forecast_conf_pct: "{{ states('sensor.pv_forecast_confidence') | float(100) }}"
aggressive: "{{ is_state('sensor.pv_muss_aggressiv_laden_v2', 'on') }}"
sun_azimuth: "{{ state_attr('sun.sun', 'azimuth') | float(0) }}"
sunset_ts: "{{ as_timestamp(states('sensor.sun_next_setting')) }}"
deadline_ts: "{{ sunset_ts - 2700 }}"
now_ts: "{{ as_timestamp(now()) }}"
hours_left: "{{ max((deadline_ts - now_ts) / 3600, 0.25) }}"
current_limit_w: "{{ states('input_number.charging_power') | float(300) }}"
confidence_factor: "{{ max(forecast_conf_pct / 100, 0.45) }}"
required_avg_w: |
{% if missing_kwh <= 0 %}
300
{% else %}
{{ ((missing_kwh * 1000) / hours_left) | round(0) }}
{% endif %}
urgency_ratio: >
{% set denom = max(net_rest_kwh, 0.1) %} {{ (missing_kwh / denom) | round(2)
}}
normal_target_w: >
{% set base = (required_avg_w | float(0)) / (confidence_factor | float(1))
%} {{ base | round(0) }}
aggressive_target_w: >
{% set factor = min(max(urgency_ratio | float(1), 1.25), 3.0) %} {% set base
= (required_avg_w | float(0)) * factor %} {{ base | round(0) }}
target_limit_w: |
{% if aggressive %}
{{ min(max(aggressive_target_w | float(300), 300), 7700) | round(0) }}
{% else %}
{{ min(max(normal_target_w | float(300), 300), 7700) | round(0) }}
{% endif %}
sunset_boost_active: |
{{
(missing_kwh | float(0)) > 0.15
and (lux | float(0)) < 6000
and (sun_azimuth | float(0)) >= 235
}}
adjusted_target_limit_w: |
{% if sunset_boost_active %}
{{ min((target_limit_w | float(300)) * 1.9, 7700) | round(0) }}
{% else %}
{{ target_limit_w }}
{% endif %}
delta_w: >-
{{ ((adjusted_target_limit_w | float(0)) - (current_limit_w | float(0))) |
abs }}
last_change: "{{ states('input_datetime.pv_charge_last_change') }}"
seconds_since_change: |
{% if last_change in ['unknown', 'unavailable', 'none', ''] %}
999999
{% else %}
{{ (as_timestamp(now()) - as_timestamp(strptime(last_change, '%Y-%m-%d %H:%M:%S'))) | int(999999) }}
{% endif %}
hi haysdb and thanks for your work. can you help me to use this package without the sensor.weather_station_solar_radiation?
is possible to subtitute it with a value from solcast?
substituted all occurences of bms_xxx and weather_yyy , but still have these variables not working:
sensor.abc_solar_radiation_max
sensor.abc_solar_radiation_min
sensor.abc_available_solar_energy
sensor.abc_total_available_solar_energy
sensor.abc_allocation_this_hour
sensor.abc_max_charge_current
sensor.abc_dashboard_helper
@haysdb Hi David,
Posting this in repository you create upon GitHub would be a better way to share it. Also gives you historical versioning and ability for others to contribute and add to your code.
