From 0120f6c8af2f168e724bb2caea8fcc76a95d221a Mon Sep 17 00:00:00 2001 From: fboundy Date: Thu, 14 Nov 2024 16:26:45 +0000 Subject: [PATCH 1/5] Fix bugs when limited consumption history is available --- apps/pv_opt/pv_opt.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/pv_opt/pv_opt.py b/apps/pv_opt/pv_opt.py index 640201e..9d85d9d 100644 --- a/apps/pv_opt/pv_opt.py +++ b/apps/pv_opt/pv_opt.py @@ -599,6 +599,7 @@ def _run_test(self): self._log_inverterstatus(self.inverter.status) def _check_for_io(self): + self.io = False self.ulog("Checking for Intelligent Octopus") entity_id = f"binary_sensor.octopus_energy_{self.get_config('octopus_account').lower().replace('-', '_')}_intelligent_dispatching" io_dispatches = self.get_state(entity_id) @@ -725,7 +726,7 @@ 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, + max_dod=self.get_config("maximum_dod_percent", 15) / 100, current_limit_amps=self.get_config("battery_current_limit_amps", default=100), voltage=self.get_config("battery_voltage", default=50), ) @@ -989,6 +990,7 @@ def _load_contract(self): self.rlog("") self._load_saving_events() + self._check_for_io() self.rlog("Finished loading contract") @@ -2582,8 +2584,12 @@ def load_consumption(self, start, end): consumption_dow = self.get_config("day_of_week_weighting") * dfx.iloc[: len(temp)] if len(consumption_dow) != len(consumption_mean): self.log(">>> Inconsistent lengths in consumption arrays") - self.log(f">>> dow : {consumption_dow}") - self.log(f">>> mean: {consumption_mean}") + self.log(f">>> dow : {len(consumption_dow)}") + self.log(f">>> mean: {len(consumption_mean)}") + idx = consumption_dow.index.intersection(consumption_mean.index) + self.log(f"Clipping the consumption to the overlap ({len(idx)/24:0.1f} days)", level="WARN") + consumption_mean = consumption_mean.loc[idx] + consumption_dow = consumption_dow.loc[idx] consumption["consumption"] += pd.Series( consumption_dow.to_numpy() From 1b1f23753254e147db559291bd4191a525825001 Mon Sep 17 00:00:00 2001 From: fboundy Date: Thu, 14 Nov 2024 16:52:53 +0000 Subject: [PATCH 2/5] Status debugging --- README.md | 2 +- apps/pv_opt/pv_opt.py | 8 +++++--- apps/pv_opt/solis.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ebe1409..5ef8daa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PV Opt: Home Assistant Solar/Battery Optimiser v3.18.0 +# PV Opt: Home Assistant Solar/Battery Optimiser v3.18.1

This documentation needs updating!

diff --git a/apps/pv_opt/pv_opt.py b/apps/pv_opt/pv_opt.py index 9d85d9d..68b4a8b 100644 --- a/apps/pv_opt/pv_opt.py +++ b/apps/pv_opt/pv_opt.py @@ -12,7 +12,7 @@ from numpy import nan import re -VERSION = "3.18.0" +VERSION = "3.18.1" OCTOPUS_PRODUCT_URL = r"https://api.octopus.energy/v1/products/" @@ -1568,7 +1568,9 @@ def _expose_configs(self, over_write=True): def status(self, status): entity_id = f"sensor.{self.prefix.lower()}status" attributes = {"last_updated": pd.Timestamp.now().strftime(DATE_TIME_FORMAT_LONG)} - self.set_state(state=status, entity_id=entity_id, attributes=attributes) + self.log(f">>> {status}") + self.log(f">>> {entity_id}") + self.log(f">>> {self.set_state(state=status, entity_id=entity_id, attributes=attributes)}") @ad.app_lock def optimise_state_change(self, entity_id, attribute, old, new, kwargs): @@ -2587,7 +2589,7 @@ def load_consumption(self, start, end): self.log(f">>> dow : {len(consumption_dow)}") self.log(f">>> mean: {len(consumption_mean)}") idx = consumption_dow.index.intersection(consumption_mean.index) - self.log(f"Clipping the consumption to the overlap ({len(idx)/24:0.1f} days)", level="WARN") + self.log(f"Clipping the consumption to the overlap ({len(idx)/24:0.1f} days)", level="WARNING") consumption_mean = consumption_mean.loc[idx] consumption_dow = consumption_dow.loc[idx] diff --git a/apps/pv_opt/solis.py b/apps/pv_opt/solis.py index cbba11f..ae74041 100644 --- a/apps/pv_opt/solis.py +++ b/apps/pv_opt/solis.py @@ -547,7 +547,7 @@ def _solis_control_charge_discharge(self, direction, enable, **kwargs): "start": kwargs.get("start", None), "end": kwargs.get("end", None), } - power = kwargs.get("power") + power = kwargs.get("power", 0) if times["start"] is not None: times["start"] = times["start"].floor("1min") From 319457dd00b8d4d8075177ff47a77276f8e379e6 Mon Sep 17 00:00:00 2001 From: fboundy Date: Thu, 14 Nov 2024 21:15:21 +0000 Subject: [PATCH 3/5] Add retry to soliscloud read_code --- apps/pv_opt/pv_opt.py | 2 +- apps/pv_opt/solis.py | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/apps/pv_opt/pv_opt.py b/apps/pv_opt/pv_opt.py index 68b4a8b..cc833e3 100644 --- a/apps/pv_opt/pv_opt.py +++ b/apps/pv_opt/pv_opt.py @@ -464,7 +464,7 @@ def initialize(self): self.log("") self.log(f"******************* PV Opt v{VERSION} *******************") self.log("") - + self.io = False self.debug = DEBUG self.redact_regex = REDACT_REGEX diff --git a/apps/pv_opt/solis.py b/apps/pv_opt/solis.py index ae74041..154ed6f 100644 --- a/apps/pv_opt/solis.py +++ b/apps/pv_opt/solis.py @@ -258,6 +258,8 @@ class SolisCloud: "atRead": "/v2/api/atRead", } + MAX_RETRIES = 5 + def __init__(self, username, password, key_id, key_secret, plant_id, **kwargs): self.username = username self.key_id = key_id @@ -331,14 +333,25 @@ def last_seen(self): return pd.to_datetime(int(self.inverter_details["dataTimestamp"]), unit="ms") def read_code(self, cid): - if self.token == "": - self.login() - body = self.get_body(inverterSn=self.inverter_sn, cid=cid) - headers = self.header(body, self.URLS["atRead"]) - headers["token"] = self.token - response = requests.post(self.URLS["root"] + self.URLS["atRead"], data=body, headers=headers) - if response.status_code == HTTPStatus.OK: - return response.json()["data"]["msg"] + retries = 0 + data = "ERROR" + while (data == "ERROR") and (retries < self.MAX_RETRIES): + if self.token == "": + self.login() + body = self.get_body(inverterSn=self.inverter_sn, cid=cid) + headers = self.header(body, self.URLS["atRead"]) + headers["token"] = self.token + response = requests.post(self.URLS["root"] + self.URLS["atRead"], data=body, headers=headers) + if response.status_code == HTTPStatus.OK: + data = response.json()["data"]["msg"] + else: + data = "ERROR" + + if data == "ERROR": + self.token = "" + retries += 1 + else: + return data def set_code(self, cid, value): if self.token == "": From 1ef8b3d9e3f1a2b7c307108e7c6e3851e726ff70 Mon Sep 17 00:00:00 2001 From: fboundy Date: Thu, 14 Nov 2024 21:27:59 +0000 Subject: [PATCH 4/5] Tidy up charge times when there are none --- apps/pv_opt/pv_opt.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/pv_opt/pv_opt.py b/apps/pv_opt/pv_opt.py index cc833e3..994c896 100644 --- a/apps/pv_opt/pv_opt.py +++ b/apps/pv_opt/pv_opt.py @@ -2304,9 +2304,16 @@ def _write_output(self): attributes={"Summary": self.summary_costs}, ) + if len(self.windows) > 0: + hass_start = self.charge_start_datetime + hass_end = self.charge_end_datetime + else: + hass_start = pd.Timestamp.now().floor("1D") + hass_end = hass_start + self.write_to_hass( entity=f"sensor.{self.prefix}_charge_start", - state=self.charge_start_datetime, + state=hass_start, attributes={ "friendly_name": "PV Opt Next Charge Period Start", "device_class": "timestamp", @@ -2329,7 +2336,7 @@ def _write_output(self): self.write_to_hass( entity=f"sensor.{self.prefix}_charge_end", - state=self.charge_end_datetime, + state=hass_end, attributes={ "friendly_name": "PV Opt Next Charge Period End", }, From 4a809576c813d98fc6b09f577f97639afa6be9ca Mon Sep 17 00:00:00 2001 From: fboundy Date: Fri, 15 Nov 2024 08:42:56 +0000 Subject: [PATCH 5/5] Return 0 from is_online if no network --- apps/pv_opt/pv_opt.py | 8 ++++---- apps/pv_opt/solis.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/pv_opt/pv_opt.py b/apps/pv_opt/pv_opt.py index 994c896..57daece 100644 --- a/apps/pv_opt/pv_opt.py +++ b/apps/pv_opt/pv_opt.py @@ -1566,11 +1566,11 @@ def _expose_configs(self, over_write=True): ) def status(self, status): - entity_id = f"sensor.{self.prefix.lower()}status" + entity_id = f"sensor.{self.prefix.lower()}_status" attributes = {"last_updated": pd.Timestamp.now().strftime(DATE_TIME_FORMAT_LONG)} - self.log(f">>> {status}") - self.log(f">>> {entity_id}") - self.log(f">>> {self.set_state(state=status, entity_id=entity_id, attributes=attributes)}") + # self.log(f">>> {status}") + # self.log(f">>> {entity_id}") + self.set_state(state=status, entity_id=entity_id, attributes=attributes) @ad.app_lock def optimise_state_change(self, entity_id, attribute, old, new, kwargs): diff --git a/apps/pv_opt/solis.py b/apps/pv_opt/solis.py index 154ed6f..a436fcd 100644 --- a/apps/pv_opt/solis.py +++ b/apps/pv_opt/solis.py @@ -323,6 +323,8 @@ def inverter_details(self): if response.status_code == HTTPStatus.OK: return response.json()["data"] + else: + return {"state": 0} @property def is_online(self):