Battery Charge Throttle
Posted: Fri Jul 07, 2023 1:06 pm
Battery Charge throttle
The LifePo4 (LFP) batteries used by FoxESS systems are very stable and well suited to being used as a solar home battery.
LFP batteries have a very flat voltage profile across 85% of their usable range, with a rapid fall below 10% minsoc, and a rapid increase when nearing full.
The Foxess battery charger (in the inverter) slows the charge it nears 100% SoC to avoid unnecessary heating and stress, it does this in large steps by ramping charge current often something like (4kw>2kw>1kw>off) - the BMS having access to all of the cell temperatures, voltages and SoC's has final control of the cut-off point and this is why occasionally you see the charge end whilst the battery is only 95% (ish) this happens more often with solar charge because SoC has drifted over time.
The more advanced battery chargers watch individual voltage and cell temperatures, and they reduce the charge current whilst holding charge voltage - this avoids the rapid internal temperature rise and it gives the batteries the maximum time for the charge to be absorbed safely and also helps improve top balancing.
The Foxess charger (in the inverter) ramps the charge current down as it approaches full (the BMS controls it using the BMS charge rate register) but it is not that fine a control, and often occurs too near the limit and does not appear to voltage hold whilst reducing current.
It does a reasonable job, in winter the warming of the batteries is a good thing but in summer it can add excessive heat stress and so I have adapted the charge profile using a Home Assistant automation and RS485 interface to the inverter so that it gives me a finer control, I use this to gradually taper the charge current once the battery passes 80% SoC and it has a very soft profile above 90%.
Using this automation will change the day charge profile to look as follows -
and the night charge profile as this -
The pre-requisites for this automation are having a Foxess Hybrid inverter and batteries (HV,Mira,ECS) and a Home Assistant which is connected by RS485 interface using the Foxess_modbus integration that can be found at https://github.com/nathanmarlor/foxess_modbus
I would recommend you only implement this if you are an advanced user and understand the implications of charge control.
This automation requires the creation of a number of helpers and a template sensor.
Beginning with the Helpers
The first helper is a toggle that enables or disables the charge throttle automation - I typically have it switched on all the time, the main reason I turn it off is speed of charge - reducing the battery current takes the charge much longer to complete, it will achieve 80% in the usual time it takes, whereas the final 20% can take several hours to complete - very useful when charging from solar in summer, but not so useful when charging off-grid during a short eco tariff period in winter.
Helpers can be created in Home Assistant by going into Settings, Devices & Services, click on 'Helpers' at top right of the screen, click '+ CREATE HELPER' and the select Toggle.
It should be set as follows -
The second helper is used by the automation to record what the battery_soc was last time it was triggered, and makes it not trigger again until the battery_soc changes.
As above go to create a helper, choose 'Number' and set is as follows -
The third helper is a number that is used to hold the calculated charge value in amps, it should be sets as follows -
The next helper is used to hold the maximum battery dc charge current - this is the maximum value the charge current will be for a normal charge, it is also used as the base multiplier for the charge current calculation.
To calculate the desired charge current from your normal charge rate - if you have a normal charge rate of 4kw, you need look at your nominal battery voltage (the mid point between high and low) - for example on an HV2600 pack with 4 batteries this will be approx 211V. Current (A) = Power(W) / Volts(V) so in this example 4kw = 4000W/211V = 19A and you would set the maximum charge rate to 19A
If desired you can reduce this to limit the max charge your inverter uses (useful in summer to balance charge with export), or increase it, the maxmimum charge you can achieve is usually the Hybrid inverter limit or the maximum PV string output if greater i.e. H1-3.7 is 3.7kw, H1-5.0 is 5kw.
It should be set as follows -
The final helper is used to select Day or Night mode, Day mode uses the day charge curve (longer and slower charge to make use of the full days solar, it slows charge dramatically above 80% SoC), Night mode uses the night charge curve (shorter and faster in duration to make use of low tariff grid charging, it only operates from 90% SoC).
It should be set as follows -
SENSORS
Then create a sensor that is used simply as a display to show you the current charge rate in watts - this is based on the current charge rate and battery voltage.
It looks like this
JINJA MACRO
Finally you need to create 2 Jinja imports, these are useful to create small macros that are re-usable, for example if you regularly convert temperature between C and F you can create a jinja template, store it as a macro and it becomes re-usable (similar to a macro or function in code)
in the /config/custom_templates folder create a file called charge_curve_day.jinja
copy and paste this code into the file and save it
Then in the /config/custom_templates folder create a second file called charge_curve_night.jinja
copy and paste this code into the file and save it
Once it is saved these jinja import macros are loaded at startup, or by going to Developer Tools, Services and select 'Home Assistant Core Integration: Reload custom Jinja2 templates' - click 'Call Service ' and they will be available to use.
You can test in Developer Tools, Templates with the following test (for day mode)-
It will return '1' for less than 80% battery_soc and a fraction when 80% or above
This code is the multiplier % applied to the maximum battery dc charge limit, the % stored against each value is the % used at that battery_soc for example in day mode at 85% soc the value is 0.42, in the max charge example above this would then give a charge rate of 19A * 0.42 = 7.98A (rounded to 8) and using the nominal 211v it would be charging at 1,684W
DASHBOARD CARDS
Create a card in your dashboard with the following
AUTOMATION
And finally add the automation which makes sense of all this -
Settings, Automations, '+ CREATE AUTOMATION', 'create new automation (start from scratch)', switch to YAML mode by clicking the three dots in the top right corner and 'edit in yaml', then copy this code over the top of the blank code there.
Some examples of the sensors at various battery_soc's (this is an 8 pack system, ~422V nominal)
The LifePo4 (LFP) batteries used by FoxESS systems are very stable and well suited to being used as a solar home battery.
LFP batteries have a very flat voltage profile across 85% of their usable range, with a rapid fall below 10% minsoc, and a rapid increase when nearing full.
The Foxess battery charger (in the inverter) slows the charge it nears 100% SoC to avoid unnecessary heating and stress, it does this in large steps by ramping charge current often something like (4kw>2kw>1kw>off) - the BMS having access to all of the cell temperatures, voltages and SoC's has final control of the cut-off point and this is why occasionally you see the charge end whilst the battery is only 95% (ish) this happens more often with solar charge because SoC has drifted over time.
The more advanced battery chargers watch individual voltage and cell temperatures, and they reduce the charge current whilst holding charge voltage - this avoids the rapid internal temperature rise and it gives the batteries the maximum time for the charge to be absorbed safely and also helps improve top balancing.
The Foxess charger (in the inverter) ramps the charge current down as it approaches full (the BMS controls it using the BMS charge rate register) but it is not that fine a control, and often occurs too near the limit and does not appear to voltage hold whilst reducing current.
It does a reasonable job, in winter the warming of the batteries is a good thing but in summer it can add excessive heat stress and so I have adapted the charge profile using a Home Assistant automation and RS485 interface to the inverter so that it gives me a finer control, I use this to gradually taper the charge current once the battery passes 80% SoC and it has a very soft profile above 90%.
Using this automation will change the day charge profile to look as follows -
and the night charge profile as this -
The pre-requisites for this automation are having a Foxess Hybrid inverter and batteries (HV,Mira,ECS) and a Home Assistant which is connected by RS485 interface using the Foxess_modbus integration that can be found at https://github.com/nathanmarlor/foxess_modbus
I would recommend you only implement this if you are an advanced user and understand the implications of charge control.
This automation requires the creation of a number of helpers and a template sensor.
Beginning with the Helpers
The first helper is a toggle that enables or disables the charge throttle automation - I typically have it switched on all the time, the main reason I turn it off is speed of charge - reducing the battery current takes the charge much longer to complete, it will achieve 80% in the usual time it takes, whereas the final 20% can take several hours to complete - very useful when charging from solar in summer, but not so useful when charging off-grid during a short eco tariff period in winter.
Helpers can be created in Home Assistant by going into Settings, Devices & Services, click on 'Helpers' at top right of the screen, click '+ CREATE HELPER' and the select Toggle.
It should be set as follows -
The second helper is used by the automation to record what the battery_soc was last time it was triggered, and makes it not trigger again until the battery_soc changes.
As above go to create a helper, choose 'Number' and set is as follows -
The third helper is a number that is used to hold the calculated charge value in amps, it should be sets as follows -
The next helper is used to hold the maximum battery dc charge current - this is the maximum value the charge current will be for a normal charge, it is also used as the base multiplier for the charge current calculation.
To calculate the desired charge current from your normal charge rate - if you have a normal charge rate of 4kw, you need look at your nominal battery voltage (the mid point between high and low) - for example on an HV2600 pack with 4 batteries this will be approx 211V. Current (A) = Power(W) / Volts(V) so in this example 4kw = 4000W/211V = 19A and you would set the maximum charge rate to 19A
If desired you can reduce this to limit the max charge your inverter uses (useful in summer to balance charge with export), or increase it, the maxmimum charge you can achieve is usually the Hybrid inverter limit or the maximum PV string output if greater i.e. H1-3.7 is 3.7kw, H1-5.0 is 5kw.
It should be set as follows -
The final helper is used to select Day or Night mode, Day mode uses the day charge curve (longer and slower charge to make use of the full days solar, it slows charge dramatically above 80% SoC), Night mode uses the night charge curve (shorter and faster in duration to make use of low tariff grid charging, it only operates from 90% SoC).
It should be set as follows -
SENSORS
Then create a sensor that is used simply as a display to show you the current charge rate in watts - this is based on the current charge rate and battery voltage.
It looks like this
Code: Select all
- name: "Current charge rate"
unit_of_measurement: "W"
device_class: power
state: >
{{ (( ( (states('sensor.batvolt') | float(0)) * states('number.max_charge_current') | float(0)))) | round(0) }}
JINJA MACRO
Finally you need to create 2 Jinja imports, these are useful to create small macros that are re-usable, for example if you regularly convert temperature between C and F you can create a jinja template, store it as a macro and it becomes re-usable (similar to a macro or function in code)
in the /config/custom_templates folder create a file called charge_curve_day.jinja
copy and paste this code into the file and save it
Code: Select all
{% macro get_charge_limit(entity_id) %}
{% if states(entity_id)|int in [80] %}
{{ 0.8 }}
{% elif states(entity_id)|int in [81] %}
{{ 0.66 }}
{% elif states(entity_id)|int in [82] %}
{{ 0.57 }}
{% elif states(entity_id)|int in [83] %}
{{ 0.5 }}
{% elif states(entity_id)|int in [84] %}
{{ 0.45 }}
{% elif states(entity_id)|int in [85] %}
{{ 0.42 }}
{% elif states(entity_id)|int in [86] %}
{{ 0.40 }}
{% elif states(entity_id)|int in [87] %}
{{ 0.38 }}
{% elif states(entity_id)|int in [88] %}
{{ 0.36 }}
{% elif states(entity_id)|int in [89] %}
{{ 0.34 }}
{% elif states(entity_id)|int in [90] %}
{{ 0.33 }}
{% elif states(entity_id)|int in [91] %}
{{ 0.32 }}
{% elif states(entity_id)|int in [92] %}
{{ 0.30 }}
{% elif states(entity_id)|int in [93] %}
{{ 0.29 }}
{% elif states(entity_id)|int in [94] %}
{{ 0.28 }}
{% elif states(entity_id)|int in [95] %}
{{ 0.25 }}
{% elif states(entity_id)|int in [96] %}
{{ 0.24 }}
{% elif states(entity_id)|int in [97] %}
{{ 0.22 }}
{% elif states(entity_id)|int in [98] %}
{{ 0.2 }}
{% elif states(entity_id)|int in [99] %}
{{ 0.18 }}
{% elif states(entity_id)|int in [100] %}
{{ 0.15 }}
{% else %}
{{ 1 }}
{% endif %}
{% endmacro %}
Then in the /config/custom_templates folder create a second file called charge_curve_night.jinja
copy and paste this code into the file and save it
Code: Select all
{% macro get_charge_limit(entity_id) %}
{% if states(entity_id)|int in [80] %}
{{ 1 }}
{% elif states(entity_id)|int in [81] %}
{{ 1 }}
{% elif states(entity_id)|int in [82] %}
{{ 1 }}
{% elif states(entity_id)|int in [83] %}
{{ 1 }}
{% elif states(entity_id)|int in [84] %}
{{ 1 }}
{% elif states(entity_id)|int in [85] %}
{{ 1 }}
{% elif states(entity_id)|int in [86] %}
{{ 1 }}
{% elif states(entity_id)|int in [87] %}
{{ 1 }}
{% elif states(entity_id)|int in [88] %}
{{ 1 }}
{% elif states(entity_id)|int in [89] %}
{{ 1 }}
{% elif states(entity_id)|int in [90] %}
{{ 0.8 }}
{% elif states(entity_id)|int in [91] %}
{{ 0.66 }}
{% elif states(entity_id)|int in [92] %}
{{ 0.57 }}
{% elif states(entity_id)|int in [93] %}
{{ 0.49 }}
{% elif states(entity_id)|int in [94] %}
{{ 0.42 }}
{% elif states(entity_id)|int in [95] %}
{{ 0.36 }}
{% elif states(entity_id)|int in [96] %}
{{ 0.3 }}
{% elif states(entity_id)|int in [97] %}
{{ 0.24 }}
{% elif states(entity_id)|int in [98] %}
{{ 0.2 }}
{% elif states(entity_id)|int in [99] %}
{{ 0.17 }}
{% elif states(entity_id)|int in [100] %}
{{ 0.14 }}
{% else %}
{{ 1 }}
{% endif %}
{% endmacro %}
Once it is saved these jinja import macros are loaded at startup, or by going to Developer Tools, Services and select 'Home Assistant Core Integration: Reload custom Jinja2 templates' - click 'Call Service ' and they will be available to use.
You can test in Developer Tools, Templates with the following test (for day mode)-
Code: Select all
{% from 'charge_curve_day.jinja' import get_charge_limit %}
{{ get_charge_limit('sensor.battery_soc')|float(1) }}
This code is the multiplier % applied to the maximum battery dc charge limit, the % stored against each value is the % used at that battery_soc for example in day mode at 85% soc the value is 0.42, in the max charge example above this would then give a charge rate of 19A * 0.42 = 7.98A (rounded to 8) and using the nominal 211v it would be charging at 1,684W
DASHBOARD CARDS
Create a card in your dashboard with the following
Code: Select all
type: entities
entities:
- entity: sensor.battery_soc
- entity: input_number.battery_max_charge
name: Maximum Charge Limit
- entity: input_number.battery_charge_limit
name: Calculated Charge (A)
- entity: sensor.batvolt
- entity: sensor.current_charge_rate
name: Calculated Charge rate (W)
- entity: input_boolean.battery_charge_throttle
- entity: input_boolean.battery_charge_throttle_night_mode
state_color: true
AUTOMATION
And finally add the automation which makes sense of all this -
Settings, Automations, '+ CREATE AUTOMATION', 'create new automation (start from scratch)', switch to YAML mode by clicking the three dots in the top right corner and 'edit in yaml', then copy this code over the top of the blank code there.
Code: Select all
alias: Battery Charge Throttle
description: ""
trigger:
- platform: time_pattern
seconds: /30
condition:
- condition: or
conditions:
- condition: and
conditions:
- condition: or
conditions:
- condition: numeric_state
entity_id: sensor.battery_soc
above: input_number.battery_charge_last_soc
- condition: numeric_state
entity_id: sensor.battery_soc
below: input_number.battery_charge_last_soc
- condition: or
conditions:
- condition: and
conditions:
- condition: numeric_state
entity_id: sensor.battery_soc
below: 79
- condition: or
conditions:
- condition: numeric_state
entity_id: input_number.battery_charge_limit
below: input_number.battery_max_charge
- condition: numeric_state
entity_id: input_number.battery_charge_limit
above: input_number.battery_max_charge
- condition: and
conditions:
- condition: numeric_state
entity_id: sensor.battery_soc
above: 79
- condition: time
after: "07:00:00"
before: "07:01:00"
- condition: time
after: "20:00:00"
before: "20:01:00"
action:
- if:
- condition: or
conditions:
- condition: time
after: "07:00:00"
before: "07:01:00"
- condition: time
before: "20:01:00"
after: "20:00:00"
then:
- if:
- condition: time
after: "20:00:00"
before: "20:01:00"
then:
- if:
- condition: state
entity_id: input_boolean.battery_charge_throttle_night_mode
state: "off"
then:
- service: input_boolean.turn_on
data: {}
target:
entity_id: input_boolean.battery_charge_throttle_night_mode
- service: logbook.log
data:
entity_id: input_boolean.battery_charge_throttle_night_mode
message: Battery Charge Throttle Night Mode turned ON
name: Charge Throttle
domain: battery
- if:
- condition: time
after: "07:00:00"
before: "07:01:00"
then:
- if:
- condition: state
entity_id: input_boolean.battery_charge_throttle_night_mode
state: "on"
then:
- service: input_boolean.turn_off
data: {}
target:
entity_id: input_boolean.battery_charge_throttle_night_mode
- service: logbook.log
data:
entity_id: input_boolean.battery_charge_throttle_night_mode
message: Battery Charge Throttle Night Mode turned OFF
name: Charge Throttle
domain: battery
else:
- if:
- condition: and
conditions:
- condition: numeric_state
entity_id: sensor.battery_soc
above: 79
- condition: numeric_state
entity_id: sensor.battery_charge
above: 0.15
enabled: true
- condition: state
entity_id: input_boolean.battery_charge_throttle
state: "on"
then:
- if:
- condition: numeric_state
entity_id: input_number.battery_charge_limit
above: 0.1
then:
- service: input_number.set_value
data:
value: >-
{% if states('input_boolean.battery_charge_throttle_night_mode')=="on" %}
{% from 'charge_curve_night.jinja' import get_charge_limit %}
{% else %}
{% from 'charge_curve_day.jinja' import get_charge_limit %}
{% endif %}
{% set cl = get_charge_limit('sensor.battery_soc')|float(1) %}
{% if cl > 1 or cl <= 0 %}
{% set bcl = (1 * (states('input_number.battery_max_charge')|int(8)) ) %}
{% else %}
{% set bcl = (cl * (states('input_number.battery_max_charge')|int(8)) ) %}
{% endif %}
{{ bcl|round(1) }}
target:
entity_id: input_number.battery_charge_limit
- if:
- condition: template
value_template: >-
{{ (states('input_number.battery_charge_limit')|round(1)
!= states('number.max_charge_current')|round(1)) }}
then:
- service: logbook.log
data:
entity_id: number.max_charge_current
message: >-
Setting Max Charge Current ={{(states('input_number.battery_charge_limit')|round(1,0))}},
SoC={{ states( 'sensor.battery_soc' )|int }} ,
Night Mode={{states('input_boolean.battery_charge_throttle_night_mode') }}
name: Charge Current
domain: battery
- service: number.set_value
data:
value: >-
{{states('input_number.battery_charge_limit')|round(1,0) }}
target:
entity_id: number.max_charge_current
- service: logbook.log
data:
entity_id: number.max_charge_current
message: >-
Max Charge Current is set to ={{(states('number.max_charge_current')|round(1,0))}},
SoC={{ states( 'sensor.battery_soc' )|int }} , Night Mode={{states('input_boolean.battery_charge_throttle_night_mode')}}
name: Charge Current
domain: battery
else:
- service: logbook.log
data:
entity_id: number.max_charge_current
message: >-
Max Charge Current is already set to ={{(states('number.max_charge_current')|round(1,0))}} no need to set again,
SoC={{ states( 'sensor.battery_soc' )|int }} ,
Night Mode={{states('input_boolean.battery_charge_throttle_night_mode') }}
name: Charge Current
domain: battery
else:
- service: logbook.log
data:
entity_id: number.max_charge_current
message: >-
Battery Charge limit invalid ={{(states('input_number.battery_charge_limit')|float(0))|round(1)}},
SoC={{ states( 'sensor.battery_soc' )|int }}, Night Mode={{states('input_boolean.battery_charge_throttle_night_mode')}}
name: Charge Current
domain: battery
else:
- if:
- condition: or
conditions:
- condition: state
entity_id: input_boolean.battery_charge_throttle
state: "off"
- condition: numeric_state
entity_id: sensor.battery_soc
below: 79
then:
- if:
- condition: or
conditions:
- condition: numeric_state
entity_id: input_number.battery_charge_limit
below: input_number.battery_max_charge
- condition: numeric_state
entity_id: input_number.battery_charge_limit
above: input_number.battery_max_charge
then:
- service: input_number.set_value
data:
value: "{{ states('input_number.battery_max_charge')|int(8) }}"
target:
entity_id: input_number.battery_charge_limit
- service: logbook.log
data:
entity_id: number.max_charge_current
message: >-
Reset Max Charge Current ={{(states('number.max_charge_current')|float(0))|round(1)}}
name: Charge Current
domain: battery
- service: number.set_value
data:
value: >-
{{ states('input_number.battery_charge_limit')|round(1,0)}}
target:
entity_id: number.max_charge_current
- service: input_number.set_value
data:
value: >-
{% set Batsoc = states('sensor.battery_soc')|int(0) %} {{ Batsoc }}
target:
entity_id: input_number.battery_charge_last_soc
mode: single