Skip to content

Commit

Permalink
Merge pull request #251 from fboundy/patch
Browse files Browse the repository at this point in the history
3.15.4
  • Loading branch information
fboundy authored Jul 3, 2024
2 parents 6312225 + e29b19c commit aef329a
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 8 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# PV Opt: Home Assistant Solar/Battery Optimiser v3.15.3
# PV Opt: Home Assistant Solar/Battery Optimiser v3.15.4


Solar / Battery Charging Optimisation for Home Assistant. This appDaemon application attempts to optimise charging and discharging of a home solar/battery system to minimise cost electricity cost on a daily basis using freely available solar forecast data from SolCast. This is particularly beneficial for Octopus Agile but is also benefeficial for other time-of-use tariffs such as Octopus Flux or simple Economy 7.
Expand Down
4 changes: 3 additions & 1 deletion apps/pv_opt/pv_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from numpy import nan
import re

VERSION = "3.15.3"
VERSION = "3.15.4"


OCTOPUS_PRODUCT_URL = r"https://api.octopus.energy/v1/products/"
Expand Down Expand Up @@ -717,6 +717,8 @@ def _load_pv_system_model(self):
self.battery_model = pv.BatteryModel(
capacity=self.get_config("battery_capacity_wh"),
max_dod=self.get_config("maximum_dod_percent") / 100,
current_limit_amps=self.get_config("battery_current_limit_amps", default=100),
voltage=self.get_config("battery_voltage", default=50),
)
self.pv_system = pv.PVsystemModel("PV_Opt", self.inverter_model, self.battery_model, host=self)

Expand Down
58 changes: 52 additions & 6 deletions apps/pv_opt/pvpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from datetime import datetime

OCTOPUS_PRODUCT_URL = r"https://api.octopus.energy/v1/products/"
AGILE_PREDICT_URL = r"https://agilepredict.com/api/"

TIME_FORMAT = "%d/%m %H:%M %Z"
MAX_ITERS = 3

Expand Down Expand Up @@ -76,6 +78,7 @@ def __init__(
self.eco7 = eco7
self.area = kwargs.get("area", None)
self.day_ahead = None
self.agile_predict = None
self.eco7_start = pd.Timestamp(eco7_start, tz="UTC")

if octopus:
Expand Down Expand Up @@ -200,12 +203,12 @@ def to_df(self, start=None, end=None, **kwargs):
if "AGILE" in self.name and use_day_ahead:
if self.day_ahead is not None and df.index[-1].day == end.day:
# reset the day ahead forecasts if we've got a forecast going into tomorrow
self.agile_predict = None
self.day_ahead = None
self.log("")
self.log(f"Cleared day ahead forecast for tariff {self.name}")

if pd.Timestamp.now(tz=self.tz).hour > 11 and df.index[-1].day != end.day:

# if it is after 11 but we don't have new Agile prices yet, check for a day-ahead forecast
if self.day_ahead is None:
self.day_ahead = self.get_day_ahead(df.index[0])
Expand All @@ -222,7 +225,9 @@ def to_df(self, start=None, end=None, **kwargs):
else:
factors = AGILE_FACTORS["import"][self.area]

mask = (self.day_ahead.index.hour >= 16) & (self.day_ahead.index.hour < 19)
mask = (self.day_ahead.index.tz_convert("GB").hour >= 16) & (
self.day_ahead.index.tz_convert("GB").hour < 19
)

agile = (
pd.concat(
Expand All @@ -237,7 +242,13 @@ def to_df(self, start=None, end=None, **kwargs):
)

df = pd.concat([df, agile])
# self.log(df)
else:
# Otherwise download the latest forecast from AgilePredict
if self.agile_predict is None:
self.agile_predict = self._get_agile_predict()

if self.agile_predict is not None:
df = pd.concat([df, self.agile_predict.loc[df.index[-1] + pd.Timedelta("30min") : end]])

# If the index frequency >30 minutes so we need to just extend it:
if (len(df) > 1 and ((df.index[-1] - df.index[-2]).total_seconds() / 60) > 30) or len(df) == 1:
Expand Down Expand Up @@ -285,6 +296,19 @@ def to_df(self, start=None, end=None, **kwargs):

return df

def _get_agile_predict(self):
url = f"{AGILE_PREDICT_URL}{self.area}?days=2&high_low=false"
try:
r = requests.get(url)
r.raise_for_status() # Raise an exception for unsuccessful HTTP status codes

except requests.exceptions.RequestException as e:
return

df = pd.DataFrame(r.json()[0]["prices"]).set_index("date_time")
df.index = pd.to_datetime(df.index).tz_convert("UTC")
return df["agile_pred"]

def get_day_ahead(self, start):
url = "https://www.nordpoolgroup.com/api/marketdata/page/325?currency=GBP"

Expand Down Expand Up @@ -371,13 +395,34 @@ def __init__(


class BatteryModel:
def __init__(self, capacity: int, max_dod: float = 0.15) -> None:
"""Describes the battery system attached to the inverter
Attributes:
capacity: An integer describing the Wh capacity of the battery.
max_dod: A float describing the maximum depth of discharge of the battery.
current_limit_amps: An int describing the maximum amps at which the battery can charge/discharge.
voltage: An int describing the voltage of the battery system.
"""

def __init__(self, capacity: int, max_dod: float = 0.15, current_limit_amps: int = 100, voltage: int = 50) -> None:
self.capacity = capacity
self.max_dod = max_dod
self.current_limit_amps = current_limit_amps
self.voltage = voltage

def __str__(self):
pass

@property
def max_charge_power(self) -> int:
"""returns the maximum watts at which the battery can charge."""
return self.current_limit_amps * self.voltage

@property
def max_discharge_power(self) -> int:
"""returns the maximum watts at which the battery can discharge."""
return self.max_charge_power


class OctopusAccount:
def __init__(self, account_number, api_key) -> None:
Expand Down Expand Up @@ -904,7 +949,7 @@ def optimised_force(self, initial_soc, static_flows, contract: Contract, **kwarg
str_log += f"SOC: {x.loc[start_window]['soc']:5.1f}%->{x.loc[start_window]['soc_end']:5.1f}% "

forced_charge = min(
self.inverter.charger_power
min(self.battery.max_charge_power, self.inverter.charger_power)
- x["forced"].loc[start_window]
- x[cols["solar"]].loc[start_window],
((100 - x["soc_end"].loc[start_window]) / 100 * self.battery.capacity) * 2 * factor,
Expand Down Expand Up @@ -1016,7 +1061,8 @@ def optimised_force(self, initial_soc, static_flows, contract: Contract, **kwarg
slot = (
start_window,
-min(
self.inverter.inverter_power - x[kwargs.get("solar", "solar")].loc[start_window],
min(self.battery.max_discharge_power, self.inverter.inverter_power),
-x[kwargs.get("solar", "solar")].loc[start_window],
((x["soc_end"].loc[start_window] - self.battery.max_dod) / 100 * self.battery.capacity)
* 2
* factor,
Expand Down

0 comments on commit aef329a

Please sign in to comment.