Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Max charge rate temperature curve adjustments #1869

Merged
merged 23 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
94f36f7
Max charge rate temperature curve adjustments
springfall2008 Jan 11, 2025
9776829
Missing file
springfall2008 Jan 11, 2025
b99b9ec
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
89244cf
Log battery temperature
springfall2008 Jan 11, 2025
a558450
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
f98e048
Charge distribution across inverters accounted for in low power
springfall2008 Jan 11, 2025
398785c
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
0bf0c2c
Fixes for battery temperature and multi inverter low power
springfall2008 Jan 11, 2025
7b5bee7
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
ee0f758
Adding discharge temperature curve
springfall2008 Jan 11, 2025
3e75d54
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
7213ce9
Discharge curve tests
springfall2008 Jan 11, 2025
3263b28
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
412384a
Battery charge / discharge rate prediction
springfall2008 Jan 11, 2025
b39d73b
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
736eb05
Update apps.yaml
springfall2008 Jan 11, 2025
cd6c66c
Update example_chart.yml
springfall2008 Jan 11, 2025
e1cbb0a
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
79c4a84
Typo's
springfall2008 Jan 11, 2025
2fb7c7b
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
288b7bf
Update apps-yaml.md
springfall2008 Jan 11, 2025
6411c15
[pre-commit.ci lite] apply automatic fixes
pre-commit-ci-lite[bot] Jan 11, 2025
a30511f
Update apps-yaml.md
springfall2008 Jan 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions apps/predbat/config/apps.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,42 @@ pred_bat:
- select.givtcp_{geserial}_battery_pause_end_time_slot
- select.givtcp2_{geserial2}_battery_pause_end_time_slot

# Battery temperature sensor per inverter, used outside REST mode to get current temperature
#
#battery_temperature:
# - sensor.givtcp_battery_stack_1_bms_temperature
# - sensor.givtcp2_battery_stack_1_bms_temperature

# Battery temperature history, only one for modelling, used to predict future temperature
#
#battery_temperature_history: sensor.givtcp_battery_stack_1_bms_temperature

# Battery temperature charge adjustment curve
# Specific in C which is a multiple of the battery capacity
# e.g. 0.33 C is 33% of the battery capacity
# values unspecified will be assumed to be 1.0 hence rate is capped by max charge rate
#battery_temperature_charge_curve:
# 19: 0.33
# 18: 0.33
# 17: 0.33
# 16: 0.33
# 15: 0.33
# 14: 0.33
# 13: 0.33
# 12: 0.33
# 11: 0.33
# 10: 0.25
# 9: 0.25
# 8: 0.25
# 7: 0.25
# 6: 0.25
# 5: 0.25
# 4: 0.25
# 3: 0.25
# 2: 0.25
# 1: 0.15
# 0: 0.00

# Inverter max AC limit (one per inverter). E.g for a 3.6kw inverter set to 3600
# If you have a second inverter for PV only please add the two values together
inverter_limit:
Expand Down
74 changes: 47 additions & 27 deletions apps/predbat/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import sys
from datetime import datetime, timedelta
from config import MINUTE_WATT, PREDICT_STEP
from utils import dp2, dp3, calc_percent_limit, find_charge_rate
from utils import dp0, dp2, dp3, calc_percent_limit, find_charge_rate
from inverter import Inverter

"""
Expand Down Expand Up @@ -101,26 +101,35 @@ def execute_plan(self):
self.log("Inverter {} Charge window will be: {} - {} - current soc {} target {}".format(inverter.id, charge_start_time, charge_end_time, inverter.soc_percent, self.charge_limit_percent_best[0]))
# Are we actually charging?
if self.minutes_now >= minutes_start and self.minutes_now < minutes_end:
new_charge_rate = int(
find_charge_rate(
self.minutes_now,
inverter.soc_kw,
window,
self.charge_limit_percent_best[0] * inverter.soc_max / 100.0,
inverter.battery_rate_max_charge,
inverter.soc_max,
self.battery_charge_power_curve,
self.set_charge_low_power,
self.charge_low_power_margin,
self.battery_rate_min,
self.battery_rate_max_scaling,
self.battery_loss,
self.log,
)
* MINUTE_WATT
target_soc = self.charge_limit_percent_best[0] if self.charge_limit_best != self.reserve else self.soc_kw
inv_target_soc = self.adjust_battery_target_multi(inverter, target_soc, True, False, check=True)

new_charge_rate, new_charge_rate_real = find_charge_rate(
self.minutes_now,
inverter.soc_kw,
window,
inv_target_soc * inverter.soc_max / 100.0,
inverter.battery_rate_max_charge,
inverter.soc_max,
self.battery_charge_power_curve,
self.set_charge_low_power,
self.charge_low_power_margin,
self.battery_rate_min,
self.battery_rate_max_scaling,
self.battery_loss,
self.log,
inverter.battery_temperature,
self.battery_temperature_charge_curve,
)
new_charge_rate = int(new_charge_rate * MINUTE_WATT)
current_charge_rate = inverter.get_current_charge_rate()

self.log(
"Inverter {} Target SOC {} (this inverter {}) Battery temperature {} Select charge rate {}w (real {}w) current charge rate {}".format(
inverter.id, target_soc, inv_target_soc, inverter.battery_temperature, new_charge_rate, new_charge_rate_real * MINUTE_WATT, current_charge_rate
)
)

# Adjust charge rate if we are more than 10% out or we are going back to Max charge rate
max_rate = inverter.battery_rate_max_charge * MINUTE_WATT
if abs(new_charge_rate - current_charge_rate) > (0.1 * max_rate) or (new_charge_rate == max_rate):
Expand Down Expand Up @@ -519,7 +528,7 @@ def execute_plan(self):
self.isExporting = isExporting
return status, status_extra

def adjust_battery_target_multi(self, inverter, soc, is_charging, is_exporting, isFreezeCharge=False):
def adjust_battery_target_multi(self, inverter, soc, is_charging, is_exporting, isFreezeCharge=False, check=False):
"""
Adjust target SoC based on the current SoC of all the inverters accounting for their
charge rates and battery capacities
Expand All @@ -529,24 +538,30 @@ def adjust_battery_target_multi(self, inverter, soc, is_charging, is_exporting,

if isFreezeCharge:
new_soc_percent = soc
self.log("Inverter {} adjust target soc for hold to {}% based on requested all inverter soc {}%".format(inverter.id, new_soc_percent, soc))
if not check:
self.log("Inverter {} adjust target soc for hold to {}% based on requested all inverter soc {}%".format(inverter.id, new_soc_percent, soc))
elif soc == 100.0:
new_soc_percent = 100.0
self.log("Inverter {} adjust target soc for charge to {}% based on requested all inverter soc {}%".format(inverter.id, new_soc_percent, soc))
if not check:
self.log("Inverter {} adjust target soc for charge to {}% based on requested all inverter soc {}%".format(inverter.id, new_soc_percent, soc))
elif soc == 0.0:
new_soc_percent = 0.0
self.log("Inverter {} adjust target soc for export to {}% based on requested all inverter soc {}%".format(inverter.id, new_soc_percent, soc))
if not check:
self.log("Inverter {} adjust target soc for export to {}% based on requested all inverter soc {}%".format(inverter.id, new_soc_percent, soc))
else:
add_kwh = target_kwh - self.soc_kw
add_this = add_kwh * (inverter.battery_rate_max_charge / self.battery_rate_max_charge)
new_soc_kwh = max(min(inverter.soc_kw + add_this, inverter.soc_max), inverter.reserve)
new_soc_percent = calc_percent_limit(new_soc_kwh, inverter.soc_max)
self.log(
"Inverter {} adjust target soc for charge to {}% ({}kWh/{}kWh {}kWh) based on going from {}% -> {}% total add is {}kWh and this battery needs to add {}kWh to get to {}kWh".format(
inverter.id, soc, target_kwh, self.soc_max, inverter.soc_max, soc_percent, new_soc_percent, dp2(add_kwh), dp2(add_this), dp2(new_soc_kwh)
if not check:
self.log(
"Inverter {} adjust target soc for charge to {}% ({}kWh/{}kWh {}kWh) based on going from {}% -> {}% total add is {}kWh and this battery needs to add {}kWh to get to {}kWh".format(
inverter.id, soc, target_kwh, self.soc_max, inverter.soc_max, soc_percent, new_soc_percent, dp2(add_kwh), dp2(add_this), dp2(new_soc_kwh)
)
)
)
inverter.adjust_battery_target(new_soc_percent, is_charging, is_exporting)
if not check:
inverter.adjust_battery_target(new_soc_percent, is_charging, is_exporting)
return new_soc_percent

def reset_inverter(self):
"""
Expand Down Expand Up @@ -602,6 +617,7 @@ def fetch_inverter_data(self, create=True):
self.discharge_rate_now = 0.0
self.pv_power = 0
self.load_power = 0
self.battery_temperature = 0
found_first = False

if create:
Expand Down Expand Up @@ -666,6 +682,10 @@ def fetch_inverter_data(self, create=True):
self.pv_power += inverter.pv_power
self.load_power += inverter.load_power
self.current_charge_limit = calc_percent_limit(self.current_charge_limit_kwh, self.soc_max)
self.battery_temperature += inverter.battery_temperature

# Work out battery temperature
self.battery_temperature = int(dp0(self.battery_temperature / self.num_inverters))

# Remove extra decimals
self.soc_max = dp3(self.soc_max)
Expand Down
97 changes: 92 additions & 5 deletions apps/predbat/fetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -630,9 +630,9 @@ def minute_data(
if to_time:
timed_to = to_time - now

minutes = int(timed.seconds / 60) + int(timed.days * 60 * 24)
minutes = int(timed.total_seconds() / 60)
if to_time:
minutes_to = int(timed_to.seconds / 60) + int(timed_to.days * 60 * 24)
minutes_to = int(timed_to.total_seconds() / 60)

if minutes < newest_age:
newest_age = minutes
Expand Down Expand Up @@ -700,12 +700,37 @@ def minute_data(

# If we only have a start time then fill the gaps with the last values
if not to_key:
state = newest_state
# Fill from last sample until now
for minute in range(60 * 24 * days):
if backwards:
rindex = minute
else:
rindex = 60 * 24 * days - minute - 1

if rindex not in mdata:
mdata[rindex] = newest_state
else:
break

# Fill gaps before the first value
state = 0
for minute in range(60 * 24 * days):
if backwards:
rindex = 60 * 24 * days - minute - 1
else:
rindex = minute
if rindex in mdata:
state = mdata[rindex]
break

# Fill gaps in the middle
for minute in range(60 * 24 * days):
rindex = 60 * 24 * days - minute - 1
if backwards:
rindex = 60 * 24 * days - minute - 1
else:
rindex = minute
state = mdata.get(rindex, state)
mdata[rindex] = state
minute += 1

# Reverse data with smoothing
if clean_increment:
Expand Down Expand Up @@ -745,6 +770,8 @@ def fetch_sensor_data(self):
self.carbon_today_sofar = 0
self.import_today = {}
self.export_today = {}
self.battery_temperature_history = {}
self.battery_temperature_prediction = {}
self.pv_today = {}
self.load_minutes = {}
self.load_minutes_age = 0
Expand Down Expand Up @@ -806,6 +833,15 @@ def fetch_sensor_data(self):
else:
self.log("Warn: You have not set pv_today in apps.yaml, you will have no previous pv data")

# Battery temperature
if "battery_temperature_history" in self.args:
self.battery_temperature_history = self.minute_data_import_export(self.now_utc, "battery_temperature_history", scale=1.0, increment=False, smoothing=False)
data = []
for minute in range(0, 24 * 60, 5):
data.append({minute: self.battery_temperature_history.get(minute, 0)})
self.battery_temperature_prediction = self.predict_battery_temperature(self.battery_temperature_history, step=PREDICT_STEP)
self.log("Fetched battery temperature history data, current temperature {}".format(self.battery_temperature_history.get(0, None)))

# Car charging hold - when enabled battery is held during car charging in simulation
self.car_charging_energy = self.load_car_energy(self.now_utc)

Expand Down Expand Up @@ -1109,6 +1145,42 @@ def fetch_sensor_data(self):
else:
self.load_inday_adjustment = 1.0

def predict_battery_temperature(self, battery_temperature_history, step):
"""
Given historical battery temperature data, predict the future temperature

For now a fairly simple look back over 24 hours is used, can be improved with outdoor temperature later
"""

predicted_temp = {}
current_temp = battery_temperature_history.get(0, 20)
predict_timestamps = {}

for minute in range(0, self.forecast_minutes, step):
timestamp = self.now_utc + timedelta(minutes=minute)
timestamp_str = timestamp.strftime(TIME_FORMAT)
predicted_temp[minute] = dp2(current_temp)
predict_timestamps[timestamp_str] = dp2(current_temp)

# Look at 30 minute change 24 hours ago to predict the up/down trend
minute_previous = (24 * 60 - minute) % (24 * 60)
change = battery_temperature_history.get(minute_previous, 20) - battery_temperature_history.get(minute_previous + step, 20)
current_temp += change
current_temp = max(min(current_temp, 30), -20)

self.dashboard_item(
self.prefix + ".battery_temperature",
state=dp2(battery_temperature_history.get(0, 20)),
attributes={
"results": self.filtered_times(predict_timestamps),
"friendly_name": "Battery temperature",
"state_class": "measurement",
"unit_of_measurement": "c",
"icon": "mdi:temperature-celsius",
},
)
return predicted_temp

def rate_replicate(self, rates, rate_io={}, is_import=True, is_gas=False):
"""
We don't get enough hours of data for Octopus, so lets assume it repeats until told others
Expand Down Expand Up @@ -1768,6 +1840,21 @@ def fetch_config_options(self):
self.log("Warn: battery_discharge_power_curve is incorrectly configured - ignoring")
self.record_status("battery_discharge_power_curve is incorrectly configured - ignoring", had_errors=True)

# Temperature curve charge
self.battery_temperature_charge_curve = self.args.get("battery_temperature_charge_curve", {})
if not isinstance(self.battery_temperature_charge_curve, dict):
self.log("Data is {}".format(self.battery_temperature_charge_curve))
self.battery_temperature_charge_curve = {}
self.log("Warn: battery_temperature_charge_curve is incorrectly configured - ignoring")
self.record_status("battery_temperature_charge_curve is incorrectly configured - ignoring", had_errors=True)

# Temperature curve discharge
self.battery_temperature_discharge_curve = self.args.get("battery_temperature_discharge_curve", {})
if not isinstance(self.battery_temperature_discharge_curve, dict):
self.battery_temperature_discharge_curve = {}
self.log("Warn: battery_temperature_discharge_curve is incorrectly configured - ignoring")
self.record_status("battery_temperature_discharge_curve is incorrectly configured - ignoring", had_errors=True)

self.import_export_scaling = self.get_arg("import_export_scaling", 1.0)
self.best_soc_margin = 0.0
self.best_soc_min = self.get_arg("best_soc_min")
Expand Down
28 changes: 27 additions & 1 deletion apps/predbat/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=
self.battery_rate_max_discharge = 0
self.battery_rate_max_charge_scaled = 0
self.battery_rate_max_discharge_scaled = 0
self.battery_temperature = 20
self.battery_power = 0
self.battery_voltage = 52.0
self.pv_power = 0
Expand Down Expand Up @@ -251,6 +252,29 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=

# Battery size, charge and discharge rates
ivtime = None
if self.rest_data and ("Battery_Details" in self.rest_data):
average_temp = 0
battery_count = 0
battery_capacity = 0
battery_voltage = 0
for battery in self.rest_data["Battery_Details"]:
battery_details = self.rest_data["Battery_Details"][battery]
if "BMS_Temperature" in battery_details:
average_temp += float(battery_details["BMS_Temperature"])
battery_count += 1
elif "Battery_Temperature" in battery_details:
average_temp += float(battery_details["Battery_Temperature"])
battery_count += 1
else:
for item in battery_details.values():
if type(item) is dict:
if "Battery_Temperature" in item:
average_temp += float(item["Battery_Temperature"])
battery_count += 1
if battery_count > 0:
average_temp /= battery_count
self.battery_temperature = dp2(average_temp)

if self.rest_data and ("Invertor_Details" in self.rest_data):
idetails = self.rest_data["Invertor_Details"]
if "Battery_Capacity_kWh" in idetails:
Expand Down Expand Up @@ -311,6 +335,7 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=
if "Invertor_Time" in idetails:
ivtime = idetails["Invertor_Time"]
else:
self.battery_temperature = self.base.get_arg("battery_temperature", default=20, index=self.id)
self.soc_max = self.base.get_arg("soc_max", default=10.0, index=self.id) * self.battery_scaling
self.nominal_capacity = self.soc_max

Expand Down Expand Up @@ -411,7 +436,7 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=
# Log inverter details
if not quiet:
self.base.log(
"Inverter {} with soc_max {} kWh nominal_capacity {} kWh battery rate raw {} w charge rate {} kW discharge rate {} kW battery_rate_min {} w ac limit {} kW export limit {} kW reserve {} % current_reserve {} %".format(
"Inverter {} with soc_max {} kWh nominal_capacity {} kWh battery rate raw {} w charge rate {} kW discharge rate {} kW battery_rate_min {} w ac limit {} kW export limit {} kW reserve {} % current_reserve {} % temperature {} c".format(
self.id,
dp2(self.soc_max),
dp2(self.nominal_capacity),
Expand All @@ -423,6 +448,7 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=
dp2(self.export_limit * 60),
self.reserve_percent,
self.reserve_percent_current,
self.battery_temperature,
)
)

Expand Down
Loading
Loading