diff --git a/.test/solis_cloud_test.py b/.test/solis_cloud_test.py
index 2146a40..bd8be68 100644
--- a/.test/solis_cloud_test.py
+++ b/.test/solis_cloud_test.py
@@ -225,3 +225,9 @@ def set_timer(self, direction, start, end, power):
# %%
sc.set_timer("charge", pd.Timestamp("00:50"), pd.Timestamp("01:00"), 3000)
# %%
+sc.set_mode_switch(35)
+# %%
+r=sc.set_code(696, 3600)
+# %%
+
+# %%
diff --git a/LICENSE b/LICENSE
index 88f494d..048b6a9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,11 @@
+This contains code from the following projects:
+
+[solis_control](https://github.com/stevegal/solis_control)
+
+It has been modified by fboundy. The original licenses for all above projects can be found in the
+[licenses folder](https://github.com/fboundy/pv_opt/licenses).
+
+
MIT License
Copyright (c) 2023 fboundy
@@ -19,3 +27,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+
diff --git a/README.md b/README.md
index ebe1409..e67e39c 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.2
This documentation needs updating!
diff --git a/apps/pv_opt/pv_opt.py b/apps/pv_opt/pv_opt.py
index 640201e..387ce08 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.2"
OCTOPUS_PRODUCT_URL = r"https://api.octopus.energy/v1/products/"
@@ -464,10 +464,11 @@ def initialize(self):
self.log("")
self.log(f"******************* PV Opt v{VERSION} *******************")
self.log("")
-
+ self.io = False
+ self.agile = False
self.debug = DEBUG
self.redact_regex = REDACT_REGEX
-
+ self.contract_last_loaded = pd.Timestamp("1970-01-01", tz="UTC")
try:
subver = int(VERSION.split(".")[2])
except:
@@ -599,6 +600,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 +727,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 +991,7 @@ def _load_contract(self):
self.rlog("")
self._load_saving_events()
+
self._check_for_io()
self.rlog("Finished loading contract")
@@ -1564,8 +1567,10 @@ 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.set_state(state=status, entity_id=entity_id, attributes=attributes)
@ad.app_lock
@@ -2300,9 +2305,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",
@@ -2325,7 +2337,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",
},
@@ -2582,8 +2594,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="WARNING")
+ consumption_mean = consumption_mean.loc[idx]
+ consumption_dow = consumption_dow.loc[idx]
consumption["consumption"] += pd.Series(
consumption_dow.to_numpy()
diff --git a/apps/pv_opt/solis.py b/apps/pv_opt/solis.py
index cbba11f..a436fcd 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
@@ -321,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):
@@ -331,14 +335,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 == "":
@@ -547,7 +562,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")
diff --git a/licenses/pvopt license.md b/licenses/pvopt license.md
new file mode 100644
index 0000000..6bfe09f
--- /dev/null
+++ b/licenses/pvopt license.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 fboundy
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/licenses/soliscontrol license.md b/licenses/soliscontrol license.md
new file mode 100644
index 0000000..aa31383
--- /dev/null
+++ b/licenses/soliscontrol license.md
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 stevegal
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file