Skip to content

Commit

Permalink
Robustness fixes (#459)
Browse files Browse the repository at this point in the history
* Add sanity check feature
* Try not to crash on REST error
* Avoid spurious index out of range warnings
* Fix charge freeze notification issue
* FIx locations of writing the example dashboard

---------

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
  • Loading branch information
springfall2008 and pre-commit-ci-lite[bot] authored Dec 13, 2023
1 parent 18c2206 commit e53f8c7
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 19 deletions.
166 changes: 147 additions & 19 deletions apps/predbat/predbat.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import copy
import appdaemon.plugins.hass.hassapi as hass
import adbase as ad
import os
import yaml

THIS_VERSION = "v7.14.8"
THIS_VERSION = "v7.14.9"
TIME_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
TIME_FORMAT_SECONDS = "%Y-%m-%dT%H:%M:%S.%f%z"
TIME_FORMAT_OCTOPUS = "%Y-%m-%d %H:%M:%S%z"
Expand Down Expand Up @@ -1922,11 +1924,15 @@ def rest_readData(self, api="readData"):
self.base.record_status("Inverter {} unable to read REST data from {} - REST will be disabled".format(self.id, url), had_errors=True)
return None

def rest_runAll(self):
def rest_runAll(self, old_data=None):
"""
Updated and get inverter status
"""
return self.rest_readData(api="runAll")
new_data = self.rest_readData(api="runAll")
if new_data:
return new_data
else:
return old_data

def rest_setChargeTarget(self, target):
"""
Expand All @@ -1938,7 +1944,7 @@ def rest_setChargeTarget(self, target):
for retry in range(0, 5):
r = requests.post(url, json=data)
# time.sleep(10)
self.rest_data = self.rest_runAll()
self.rest_data = self.rest_runAll(self.rest_data)
if float(self.rest_data["Control"]["Target_SOC"]) == target:
self.base.log("Inverter {} charge target {} via REST successful on retry {}".format(self.id, target, retry))
return True
Expand All @@ -1957,7 +1963,7 @@ def rest_setChargeRate(self, rate):
for retry in range(0, 5):
r = requests.post(url, json=data)
# time.sleep(10)
self.rest_data = self.rest_runAll()
self.rest_data = self.rest_runAll(self.rest_data)
new = self.rest_data["Control"]["Battery_Charge_Rate"]
if abs(new - rate) < 100:
self.base.log("Inverter {} set charge rate {} via REST successful on retry {}".format(self.id, rate, retry))
Expand All @@ -1977,7 +1983,7 @@ def rest_setDischargeRate(self, rate):
for retry in range(0, 5):
r = requests.post(url, json=data)
# time.sleep(10)
self.rest_data = self.rest_runAll()
self.rest_data = self.rest_runAll(self.rest_data)
new = self.rest_data["Control"]["Battery_Discharge_Rate"]
if abs(new - rate) < 100:
self.base.log("Inverter {} set discharge rate {} via REST successful on retry {}".format(self.id, rate, retry))
Expand All @@ -1999,7 +2005,7 @@ def rest_setBatteryMode(self, inverter_mode):
for retry in range(0, 5):
r = requests.post(url, json=data)
# time.sleep(10)
self.rest_data = self.rest_runAll()
self.rest_data = self.rest_runAll(self.rest_data)
if inverter_mode == self.rest_data["Control"]["Mode"]:
self.base.log("Set inverter {} mode {} via REST successful on retry {}".format(self.id, inverter_mode, retry))
return True
Expand All @@ -2019,7 +2025,7 @@ def rest_setReserve(self, target):
for retry in range(0, 5):
r = requests.post(url, json=data)
# time.sleep(10)
self.rest_data = self.rest_runAll()
self.rest_data = self.rest_runAll(self.rest_data)
result = int(float(self.rest_data["Control"]["Battery_Power_Reserve"]))
if result == target:
self.base.log("Set inverter {} reserve {} via REST successful on retry {}".format(self.id, target, retry))
Expand All @@ -2039,7 +2045,7 @@ def rest_enableChargeSchedule(self, enable):
for retry in range(0, 5):
r = requests.post(url, json=data)
# time.sleep(10)
self.rest_data = self.rest_runAll()
self.rest_data = self.rest_runAll(self.rest_data)
new_value = self.rest_data["Control"]["Enable_Charge_Schedule"]
if isinstance(new_value, str):
if new_value.lower() in ["enable", "on", "true"]:
Expand All @@ -2064,7 +2070,7 @@ def rest_setChargeSlot1(self, start, finish):
for retry in range(0, 5):
r = requests.post(url, json=data)
# time.sleep(10)
self.rest_data = self.rest_runAll()
self.rest_data = self.rest_runAll(self.rest_data)
if self.rest_data["Timeslots"]["Charge_start_time_slot_1"] == start and self.rest_data["Timeslots"]["Charge_end_time_slot_1"] == finish:
self.base.log("Inverter {} set charge slot 1 {} via REST successful after retry {}".format(self.id, data, retry))
return True
Expand All @@ -2083,7 +2089,7 @@ def rest_setDischargeSlot1(self, start, finish):
for retry in range(0, 5):
r = requests.post(url, json=data)
# time.sleep(10)
self.rest_data = self.rest_runAll()
self.rest_data = self.rest_runAll(self.rest_data)
if self.rest_data["Timeslots"]["Discharge_start_time_slot_1"] == start and self.rest_data["Timeslots"]["Discharge_end_time_slot_1"] == finish:
self.base.log("Inverter {} Set discharge slot 1 {} via REST successful after retry {}".format(self.id, data, retry))
return True
Expand Down Expand Up @@ -2172,6 +2178,9 @@ def get_arg(self, arg, default=None, indirect=True, combine=False, attribute=Non

# Resolve locally if no HA config
if value is None:
if (arg not in self.args) and default and (index is not None):
# Allow default to apply to all indices if there is not config item set
index = None
value = self.args.get(arg, default)
value = self.resolve_arg(arg, value, default=default, indirect=indirect, combine=combine, attribute=attribute, index=index)

Expand Down Expand Up @@ -8124,11 +8133,12 @@ def execute_plan(self):

if self.set_charge_freeze and self.charge_limit_best[0] == self.reserve:
status = "Freeze charging"
status_extra = " target {}%".format(inverter.soc_percent)
else:
status = "Charging"
status_extra = " target {}%".format(self.charge_limit_percent_best[0])

isCharging = True
status_extra = " target {}%".format(self.charge_limit_percent_best[0])

# Hold charge mode when enabled
if (
Expand Down Expand Up @@ -9356,14 +9366,27 @@ def create_entity_list(self):
for entity in self.dashboard_index:
text += " - entity: " + entity + "\n"

filename = "/config/predbat_dashboard.yaml"
han = open(filename, "w")
if han:
self.log("Creating predbat dashboard at {}".format(filename))
han.write(text)
han.close()
# Find path
basename = "/predbat_dashboard.yaml"
filename = None
if os.path.exists("/homeassistant"):
filename = "/homeassistant" + basename
elif os.path.exists("/config"):
filename = "/config" + basename
elif os.path.exists("/conf"):
filename = "/conf" + basename

# Write
if filename:
han = open(filename, "w")
if han:
self.log("Creating predbat dashboard at {}".format(filename))
han.write(text)
han.close()
else:
self.log("Failed to write predbat dashboard to {}".format(filename))
else:
self.log("Failed to write predbat dashboard to {}".format(filename))
self.log("Failed to write predbat dashboard as can not find /config or /conf")

def load_user_config(self, quiet=True, register=False):
"""
Expand Down Expand Up @@ -9536,6 +9559,110 @@ def auto_config(self):
for key in disabled:
del self.args[key]

def sanity(self):
"""
Sanity check appdaemon setup
"""
self.log("Sanity check:")
self.log("Sanity files in '/config' {}".format(os.listdir("/config")))
app_dirs = ["/config"]
passed = True

if os.path.exists("/config/appdaemon.yaml"):
with open("/config/appdaemon.yaml", "r") as han:
data = None

try:
data = yaml.safe_load(han)
except yaml.YAMLError as exc:
self.log("ERROR: Unable to read /config/appdaemon.yaml file correctly!")
passed = False

if data and ("appdaemon" in data):
sub_data = data["appdaemon"]
if "app_dir" in sub_data:
app_dir = sub_data["app_dir"]
app_dirs.append(app_dir)
self.log("Sanity: Got app_dir {}".format(app_dir))
else:
self.log("WARN: app_dir is not set in appdaemon.yaml")
passed = False
elif data:
self.log("WARN: appdaemon section is missing from appdaemon.yaml")
passed = False

self.log("Sanity: Scanning app_dirs {}".format(app_dirs))
apps_yaml = []
predbat_py = []
for dir in app_dirs:
for root, dirs, files in os.walk(dir):
for name in files:
filepath = os.path.join(root, name)
if name == "apps.yaml":
self.log("Sanity: Got apps.yaml in location {}".format(filepath))
apps_yaml.append(filepath)
elif name == "predbat.py":
self.log("Sanity: Got predbat.py in location {}".format(filepath))
predbat_py.append(filepath)

if not apps_yaml:
self.log("WARN: Unable to find any apps.yaml files, please check your configuration")
passed = False

# Check apps.yaml to find predbat configuration
validPred = 0
for filename in apps_yaml:
with open(filename, "r") as han:
data = None
try:
data = yaml.safe_load(han)
except yaml.YAMLError as exc:
self.log("ERROR: Unable to read {} file correctly!".format(filename))
passed = False
if data:
if "pred_bat" in data:
self.log("Sanity: {} is a valid pred_bat configuration".format(filename))
validPred += 1
if not validPred:
self.log("WARN: Unable to find any valid Predbat configurations")
passed = False
if validPred > 1:
self.log("WARN: You have multiple valid Predbat configurations")
passed = False

# Check predbat.py
if not predbat_py:
self.log("WARN: Unable to find predbat.py, please check your configuration")
passed = False
elif len(predbat_py) > 1:
self.log("WARN: Found multiple predbat.py files, please check your configuration")
passed = False
else:
filename = predbat_py[0]
foundVersion = False
with open(filename, "r") as han:
for line in han:
if "THIS_VERSION" in line:
res = re.search('THIS_VERSION\s+=\s+"([0-9.v]+)"', line)
if res:
version = res.group(1)
if version != THIS_VERSION:
self.log("WARN: The version in predbat.py is {} but this code is version {} - please re-start appdaemon".format(version, THIS_VERSION))
passed = False
else:
self.log("Sanity: Confirmed correct version {} is in predbat.py".format(version))
foundVersion = True
if not foundVersion:
self.log("WARN: Unable to find THIS_VERSION in Predbat.py file, please check your setup")
passed = False

if passed:
self.log("Sanity check has passed")
else:
self.log("Sanity check FAILED!")
self.record_status("WARN: Sanity startup checked has FAILED, see your logs for details")
return passed

def initialize(self):
"""
Setup the app, called once each time the app starts
Expand All @@ -9545,6 +9672,7 @@ def initialize(self):

try:
self.reset()
self.sanity()
self.auto_config()
self.load_user_config(quiet=False, register=True)
except Exception as e:
Expand Down
4 changes: 4 additions & 0 deletions docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ the correct mode of operation for your system.
Note that future updates to Predbat will not overwrite apps.yaml, but you may need to copy settings for new features across manually
When Predbat starts up initially it will perform a sanity check of the appDaemon configuration itself and confirm the right files are present.
You will see this check in the log, should it fail a warning will be issued and predbat.status will also reflect the warning.
While the above warning might not prevent Predbat from startup you should fix the issue ASAP as it may cause future problems.
## Predbat manual install
Note: **Not recommended if you have HACS**
Expand Down

0 comments on commit e53f8c7

Please sign in to comment.