From 0128bce96882cd2ceebc37814339acc0cbb750be Mon Sep 17 00:00:00 2001 From: fboundy Date: Sat, 24 Feb 2024 09:38:39 +0000 Subject: [PATCH] 3.9.2 - Handle no Export tariff --- README.md | 2 +- apps/pv_opt/config/config.yaml | 4 +-- apps/pv_opt/pv_opt.py | 46 +++++++++++++++++++--------------- apps/pv_opt/pvpy.py | 45 ++++++++++++++++++++------------- 4 files changed, 56 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index ae7e69e..255541a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PV Opt: Home Assistant Solar/Battery Optimiser v3.9.1 +# PV Opt: Home Assistant Solar/Battery Optimiser v3.9.2 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. diff --git a/apps/pv_opt/config/config.yaml b/apps/pv_opt/config/config.yaml index 7387ab0..e613d94 100644 --- a/apps/pv_opt/config/config.yaml +++ b/apps/pv_opt/config/config.yaml @@ -81,7 +81,7 @@ pv_opt: # Octopus account parameters # ======================================== - # octopus_auto: False # Read tariffs from the Octopus Energy integration. If successful this over-rides the following parameters + octopus_auto: False # Read tariffs from the Octopus Energy integration. If successful this over-rides the following parameters # octopus_account: !secret octopus_account # octopus_api_key: !secret octopus_api_key @@ -91,7 +91,7 @@ pv_opt: # octopus_import_tariff_code: E-2R-VAR-22-11-01-G # octopus_export_tariff_code: E-1R-AGILE-OUTGOING-19-05-13-G - # octopus_import_tariff_code: E-1R-AGILE-FLEX-22-11-25-G + octopus_import_tariff_code: E-1R-AGILE-23-12-06-G # octopus_export_tariff_code: E-1R-OUTGOING-LITE-FIX-12M-23-09-12-G # octopus_import_tariff_code: E-1R-FLUX-IMPORT-23-02-14-G diff --git a/apps/pv_opt/pv_opt.py b/apps/pv_opt/pv_opt.py index f49ffe8..79e348a 100644 --- a/apps/pv_opt/pv_opt.py +++ b/apps/pv_opt/pv_opt.py @@ -20,7 +20,7 @@ # USE_TARIFF = True -VERSION = "3.9.1" +VERSION = "3.9.2" DEBUG = False DATE_TIME_FORMAT_LONG = "%Y-%m-%d %H:%M:%S%z" @@ -724,7 +724,8 @@ def _load_contract(self): raise ValueError(e) else: - # self._check_tariffs() + if self.contract.tariffs["export"] is None: + self.contract.tariffs["export"] = pv.Tariff("None", export=True, unit=0, octopus=False, host=self) self.rlog("") self._load_saving_events() @@ -738,27 +739,28 @@ def _check_tariffs(self): self.log("Checking tariff start and end times:") self.log("------------------------------------") tariff_error = False - for tariff in self.contract.tariffs: + for direction in self.contract.tariffs: # for imp_exp, t in zip(IMPEXP, [self.contract.imp, self.contract.exp]): - t = self.contract.tariffs[tariff] - try: - z = t.end().strftime(DATE_TIME_FORMAT_LONG) - if t.end() < pd.Timestamp.now(tz="UTC"): - z = z + " <<< ERROR: Tariff end datetime in past" - tariff_error = True + tariff = self.contract.tariffs[direction] + if tariff is not None: + try: + z = tariff.end().strftime(DATE_TIME_FORMAT_LONG) + if tariff.end() < pd.Timestamp.now(tz="UTC"): + z = z + " <<< ERROR: Tariff end datetime in past" + tariff_error = True - except: - z = "N/A" + except: + z = "N/A" - if t.start() > pd.Timestamp.now(tz="UTC"): - z = z + " <<< ERROR: Tariff start datetime in future" - tariff_error = True + if tariff.start() > pd.Timestamp.now(tz="UTC"): + z = z + " <<< ERROR: Tariff start datetime in future" + tariff_error = True - self.log( - f" {tariff.title()}: {t.name:40s} Start: {t.start().strftime(DATE_TIME_FORMAT_LONG)} End: {z} " - ) - if "AGILE" in t.name: - self.agile = True + self.log( + f" {direction.title()}: {tariff.name:40s} Start: {tariff.start().strftime(DATE_TIME_FORMAT_LONG)} End: {z} " + ) + if "AGILE" in tariff.name: + self.agile = True if self.agile: self.log(" AGILE tariff detected. Rates will update at 16:00 daily") @@ -2255,7 +2257,11 @@ def _check_tariffs_vs_bottlecap(self): self.log("-----------------------------------------------------") for direction in self.contract.tariffs: if self.bottlecap_entities[direction] is None: - str_log = "No OE Integration entity found" + str_log = "No OE Integration entity found." + + elif self.contract.tariffs[direction].name == "None": + str_log = "No export tariff." + else: df = pd.DataFrame(self.get_state(self.bottlecap_entities[direction], attribute=("rates"))).set_index('start')['value_inc_vat'] df.index = pd.to_datetime(df.index) diff --git a/apps/pv_opt/pvpy.py b/apps/pv_opt/pvpy.py index f11cf96..e4aaf7e 100644 --- a/apps/pv_opt/pvpy.py +++ b/apps/pv_opt/pvpy.py @@ -17,8 +17,9 @@ def __init__( self, name, export=False, - fixed=None, - unit=None, + fixed=0, + unit=0, + valid_from = pd.Timestamp.now(tz="UTC").normalize() - pd.Timedelta(hours=24), day=None, night=None, eco7=False, @@ -44,10 +45,11 @@ def __init__( self.get_octopus(**kwargs) else: - self.fixed = fixed - self.unit = unit - self.day = day - self.night = night + self.fixed = [{'value_inc_vat': fixed, 'valid_from': valid_from}] + self.unit = [{'value_inc_vat': unit, 'valid_from': valid_from}] + if eco7: + self.day = [{'value_inc_vat': day, 'valid_from': valid_from}] + self.night = [{'value_inc_vat': night, 'valid_from': valid_from}] def _oct_time(self, d): # print(d) @@ -467,7 +469,8 @@ def net_cost(self, grid_flow, **kwargs): nc += imp_df["unit"] * grid_imp / 2000 if kwargs.get("log"): self.log(f">>> Export{self.tariffs['export'].to_df(start,end).to_string()}") - nc += self.tariffs["export"].to_df(start, end, **kwargs)["unit"] * grid_exp / 2000 + if self.tariffs["export"] is not None: + nc += self.tariffs["export"].to_df(start, end, **kwargs)["unit"] * grid_exp / 2000 return nc @@ -574,22 +577,23 @@ def optimised_force(self, initial_soc, static_flows, contract: Contract, **kwarg prices = pd.DataFrame() for direction in contract.tariffs: - prices = pd.concat( - [ - prices, - contract.tariffs[direction].to_df( - start=static_flows.index[0], end=static_flows.index[-1] - )["unit"], - ], - axis=1, - ) + if contract.tariffs[direction] is not None: + prices = pd.concat( + [ + prices, + contract.tariffs[direction].to_df( + start=static_flows.index[0], end=static_flows.index[-1] + )["unit"], + ], + axis=1, + ) if log: self.log( f" Optimiser prices loaded for period {prices.index[0].strftime(TIME_FORMAT)} - {prices.index[-1].strftime(TIME_FORMAT)}" ) - prices = prices.set_axis(contract.tariffs.keys(), axis=1) + prices = prices.set_axis([t for t in contract.tariffs.keys() if contract.tariffs[t] is not None], axis=1) df = pd.concat( [prices, consumption, self.flows(initial_soc, static_flows, **kwargs)], @@ -812,7 +816,12 @@ def optimised_force(self, initial_soc, static_flows, contract: Contract, **kwarg ) slots_added = 999 - j = 0 + # Only do the rest if there is an export tariff: + if prices['export'].sum() >0: + j = 0 + else: + j = max_iters + while (slots_added > 0) and (j < max_iters): slots_added = 0