From 55db17e4fe415453896525a2713ec403264f6b94 Mon Sep 17 00:00:00 2001 From: chappo Date: Tue, 7 Jun 2022 13:35:06 +0800 Subject: [PATCH 01/48] Set Power to Zero if Switched Off Set power to Zero if the actor is switched off. --- cbpi/extension/mqtt_actor/generic_mqtt_actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cbpi/extension/mqtt_actor/generic_mqtt_actor.py b/cbpi/extension/mqtt_actor/generic_mqtt_actor.py index c1a5b17..0d2681e 100644 --- a/cbpi/extension/mqtt_actor/generic_mqtt_actor.py +++ b/cbpi/extension/mqtt_actor/generic_mqtt_actor.py @@ -32,6 +32,6 @@ class GenericMqttActor(MQTTActor): self.state = True async def off(self): - formatted_payload = self.payload.format(switch_onoff = "off", switch_10 = 0, power = self.power) + formatted_payload = self.payload.format(switch_onoff = "off", switch_10 = 0, power = 0) await self.publish_mqtt_message(self.topic, formatted_payload) - self.state = False \ No newline at end of file + self.state = False From a4f1de3953fdb5af7ffade0b7915a18e7b4e0630 Mon Sep 17 00:00:00 2001 From: chappo Date: Tue, 7 Jun 2022 13:35:32 +0800 Subject: [PATCH 02/48] Set power to Zero if the actor is switched off. Set power to Zero if the actor is switched off. --- cbpi/extension/mqtt_actor/mqtt_actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cbpi/extension/mqtt_actor/mqtt_actor.py b/cbpi/extension/mqtt_actor/mqtt_actor.py index 28dd198..5c651e2 100644 --- a/cbpi/extension/mqtt_actor/mqtt_actor.py +++ b/cbpi/extension/mqtt_actor/mqtt_actor.py @@ -39,7 +39,7 @@ class MQTTActor(CBPiActor): async def off(self): self.state = False await self.cbpi.satellite.publish(self.topic, json.dumps( - {"state": "off", "power": self.power}), True) + {"state": "off", "power": 0}), True) pass async def run(self): @@ -56,4 +56,4 @@ class MQTTActor(CBPiActor): else: await self.off() await self.cbpi.actor.actor_update(self.id,power) - pass \ No newline at end of file + pass From 4d85c4c6affdc336ce420aa741cd863febfc6b8d Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sat, 16 Jul 2022 10:26:32 +0200 Subject: [PATCH 03/48] Update minor version --- .devcontainer/cbpi-dev-config/config.json | 16 ++++++++++++++++ .gitignore | 1 + cbpi/__init__.py | 2 +- testversion.py | 13 +++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 testversion.py diff --git a/.devcontainer/cbpi-dev-config/config.json b/.devcontainer/cbpi-dev-config/config.json index f6899f3..a94c57a 100644 --- a/.devcontainer/cbpi-dev-config/config.json +++ b/.devcontainer/cbpi-dev-config/config.json @@ -177,6 +177,22 @@ "type": "select", "value": 0 }, + "PRESSURE_UNIT": { + "description": "Set unit for pressure", + "name": "PRESSURE_UNIT", + "options": [ + { + "label": "kPa", + "value": "kPa" + }, + { + "label": "PSI", + "value": "PSI" + } + ], + "type": "select", + "value": "kPa" + }, "RECIPE_CREATION_PATH": { "description": "API path to creation plugin. Default: upload . CHANGE ONLY IF USING A RECIPE CREATION PLUGIN", "name": "RECIPE_CREATION_PATH", diff --git a/.gitignore b/.gitignore index 6349f01..bb723ac 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist .idea *.log cbpi.egg-info +cbpi4.egg-info log venv cbpi/extension/ui diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 2e6c461..d8703f2 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.6" +__version__ = "4.0.7.a1" __codename__ = "Spring Break" diff --git a/testversion.py b/testversion.py new file mode 100644 index 0000000..0baaf67 --- /dev/null +++ b/testversion.py @@ -0,0 +1,13 @@ +import json +import sys +from urllib import request +from pkg_resources import parse_version + +def versions(pkg_name): + url = f'https://pypi.python.org/pypi/{pkg_name}/json' + releases = json.loads(request.urlopen(url).read())['releases'] + releases = sorted(releases, key=parse_version, reverse=True) + return [releases[0]] + +if __name__ == '__main__': + print(*versions(sys.argv[1]), sep='\n') \ No newline at end of file From c85b929b7551b0f1e1b17a2385e7a92b19e09fbc Mon Sep 17 00:00:00 2001 From: phylax2020 Date: Tue, 19 Jul 2022 18:27:10 +0200 Subject: [PATCH 04/48] Annotations for logfile maximum size revised --- cbpi/extension/ConfigUpdate/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cbpi/extension/ConfigUpdate/__init__.py b/cbpi/extension/ConfigUpdate/__init__.py index 77d9677..9e78ae3 100644 --- a/cbpi/extension/ConfigUpdate/__init__.py +++ b/cbpi/extension/ConfigUpdate/__init__.py @@ -297,9 +297,9 @@ class ConfigUpdate(CBPiExtension): # check if SENSOR_LOG_MAX_BYTES exists in config if SENSOR_LOG_MAX_BYTES is None: - logger.info("INIT SENSOR_LOG_MAX_BYTES") + logger.info("Init maximum size of sensor logfiles") try: - await self.cbpi.config.add("SENSOR_LOG_MAX_BYTES", 100000, ConfigType.NUMBER, "Max. number of bytes in sensor logs") + await self.cbpi.config.add("SENSOR_LOG_MAX_BYTES", 100000, ConfigType.NUMBER, "Max. number of bytes in sensor logfiles") except: logger.warning('Unable to update database') From 313f9d4b398006f95ec51fb417b8694c4d494f73 Mon Sep 17 00:00:00 2001 From: phylax2020 Date: Tue, 19 Jul 2022 20:28:36 +0200 Subject: [PATCH 05/48] Set default logfile size to 100000 bytes --- cbpi/controller/log_file_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py index 993a1f5..968929f 100644 --- a/cbpi/controller/log_file_controller.py +++ b/cbpi/controller/log_file_controller.py @@ -31,7 +31,7 @@ class LogController: self.influxdb = self.cbpi.config.get("INFLUXDB", "No") if self.logfiles == "Yes": if name not in self.datalogger: - max_bytes = int(self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 131072)) + max_bytes = int(self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 100000)) backup_count = int(self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3)) data_logger = logging.getLogger('cbpi.sensor.%s' % name) From 752d9a27b64fb8b95b16321ab1c8bd34dfe42340 Mon Sep 17 00:00:00 2001 From: phylax2020 Date: Tue, 19 Jul 2022 20:45:26 +0200 Subject: [PATCH 06/48] delete log_file_controller.py. will be later restored --- cbpi/controller/log_file_controller.py | 224 ------------------------- 1 file changed, 224 deletions(-) delete mode 100644 cbpi/controller/log_file_controller.py diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py deleted file mode 100644 index 968929f..0000000 --- a/cbpi/controller/log_file_controller.py +++ /dev/null @@ -1,224 +0,0 @@ -import datetime -import glob -import logging -import os -from logging.handlers import RotatingFileHandler -from time import strftime, localtime -import pandas as pd -import zipfile -import base64 -import urllib3 -from cbpi.api import * -from cbpi.api.config import ConfigType -from cbpi.api.base import CBPiBase -import asyncio - - -class LogController: - - def __init__(self, cbpi): - ''' - - :param cbpi: craftbeerpi object - ''' - self.cbpi = cbpi - self.logger = logging.getLogger(__name__) - self.configuration = False - self.datalogger = {} - - def log_data(self, name: str, value: str) -> None: - self.logfiles = self.cbpi.config.get("CSVLOGFILES", "Yes") - self.influxdb = self.cbpi.config.get("INFLUXDB", "No") - if self.logfiles == "Yes": - if name not in self.datalogger: - max_bytes = int(self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 100000)) - backup_count = int(self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3)) - - data_logger = logging.getLogger('cbpi.sensor.%s' % name) - data_logger.propagate = False - data_logger.setLevel(logging.DEBUG) - handler = RotatingFileHandler('./logs/sensor_%s.log' % name, maxBytes=max_bytes, backupCount=backup_count) - data_logger.addHandler(handler) - self.datalogger[name] = data_logger - - formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime()) - self.datalogger[name].info("%s,%s" % (formatted_time, str(value))) - if self.influxdb == "Yes": - self.influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", "No") - self.influxdbaddr = self.cbpi.config.get("INFLUXDBADDR", None) - self.influxdbport = self.cbpi.config.get("INFLUXDBPORT", None) - self.influxdbname = self.cbpi.config.get("INFLUXDBNAME", None) - self.influxdbuser = self.cbpi.config.get("INFLUXDBUSER", None) - self.influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None) - - id = name - try: - chars = {'ö':'oe','ä':'ae','ü':'ue','Ö':'Oe','Ä':'Ae','Ü':'Ue'} - sensor=self.cbpi.sensor.find_by_id(name) - if sensor is not None: - itemname=sensor.name.replace(" ", "_") - for char in chars: - itemname = itemname.replace(char,chars[char]) - out="measurement,source=" + itemname + ",itemID=" + str(id) + " value="+str(value) - except Exception as e: - logging.error("InfluxDB ID Error: {}".format(e)) - - if self.influxdbcloud == "Yes": - self.influxdburl="https://" + self.influxdbaddr + "/api/v2/write?org=" + self.influxdbuser + "&bucket=" + self.influxdbname + "&precision=s" - try: - header = {'User-Agent': name, 'Authorization': "Token {}".format(self.influxdbpwd)} - http = urllib3.PoolManager() - req = http.request('POST',self.influxdburl, body=out, headers = header) - except Exception as e: - logging.error("InfluxDB cloud write Error: {}".format(e)) - - else: - self.base64string = base64.b64encode(('%s:%s' % (self.influxdbuser,self.influxdbpwd)).encode()) - self.influxdburl='http://' + self.influxdbaddr + ':' + str(self.influxdbport) + '/write?db=' + self.influxdbname - try: - header = {'User-Agent': name, 'Content-Type': 'application/x-www-form-urlencoded','Authorization': 'Basic %s' % self.base64string.decode('utf-8')} - http = urllib3.PoolManager() - req = http.request('POST',self.influxdburl, body=out, headers = header) - except Exception as e: - logging.error("InfluxDB write Error: {}".format(e)) - - - - async def get_data(self, names, sample_rate='60s'): - logging.info("Start Log for {}".format(names)) - ''' - :param names: name as string or list of names as string - :param sample_rate: rate for resampling the data - :return: - ''' - # make string to array - if isinstance(names, list) is False: - names = [names] - - # remove duplicates - names = set(names) - - - result = None - - def dateparse(time_in_secs): - ''' - Internal helper for date parsing - :param time_in_secs: - :return: - ''' - return datetime.datetime.strptime(time_in_secs, '%Y-%m-%d %H:%M:%S') - - def datetime_to_str(o): - if isinstance(o, datetime.datetime): - return o.__str__() - - for name in names: - # get all log names - all_filenames = glob.glob('./logs/sensor_%s.log*' % name) - # concat all logs - df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', name], header=None) for f in all_filenames]) - logging.info("Read all files for {}".format(names)) - # resample if rate provided - if sample_rate is not None: - df = df[name].resample(sample_rate).max() - logging.info("Sampled now for {}".format(names)) - df = df.dropna() - # take every nth row so that total number of rows does not exceed max_rows * 2 - max_rows = 500 - total_rows = df.shape[0] - if (total_rows > 0) and (total_rows > max_rows): - nth = int(total_rows/max_rows) - if nth > 1: - df = df.iloc[::nth] - - if result is None: - result = df - else: - result = pd.merge(result, df, how='outer', left_index=True, right_index=True) - - data = {"time": df.index.tolist()} - - if len(names) > 1: - for name in names: - data[name] = result[name].interpolate(limit_direction='both', limit=10).tolist() - else: - data[name] = result.interpolate().tolist() - - logging.info("Send Log for {}".format(names)) - - return data - - async def get_data2(self, ids) -> dict: - def dateparse(time_in_secs): - return datetime.datetime.strptime(time_in_secs, '%Y-%m-%d %H:%M:%S') - - result = dict() - for id in ids: - # df = pd.read_csv("./logs/sensor_%s.log" % id, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime',"Values"], header=None) - # concat all logs - all_filenames = glob.glob('./logs/sensor_%s.log*' % id) - df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Values'], header=None) for f in all_filenames]) - df = df.resample('60s').max() - df = df.dropna() - result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()} - return result - - - - def get_logfile_names(self, name:str ) -> list: - ''' - Get all log file names - :param name: log name as string. pattern /logs/sensor_%s.log* - :return: list of log file names - ''' - - return [os.path.basename(x) for x in glob.glob('./logs/sensor_%s.log*' % name)] - - def clear_log(self, name:str ) -> str: - - all_filenames = glob.glob('./logs/sensor_%s.log*' % name) - for f in all_filenames: - os.remove(f) - - if name in self.datalogger: - del self.datalogger[name] - - - def get_all_zip_file_names(self, name: str) -> list: - - ''' - Return a list of all zip file names - :param name: - :return: - ''' - - return [os.path.basename(x) for x in glob.glob('./logs/*-sensor-%s.zip' % name)] - - def clear_zip(self, name:str ) -> None: - """ - clear all zip files for a sensor - :param name: sensor name - :return: None - """ - - all_filenames = glob.glob('./logs/*-sensor-%s.zip' % name) - for f in all_filenames: - os.remove(f) - - def zip_log_data(self, name: str) -> str: - """ - :param name: sensor name - :return: zip_file_name - """ - - formatted_time = strftime("%Y-%m-%d-%H_%M_%S", localtime()) - file_name = './logs/%s-sensor-%s.zip' % (formatted_time, name) - zip = zipfile.ZipFile(file_name, 'w', zipfile.ZIP_DEFLATED) - all_filenames = glob.glob('./logs/sensor_%s.log*' % name) - for f in all_filenames: - zip.write(os.path.join(f)) - zip.close() - return os.path.basename(file_name) - - From ff0ba76cc0188eaccae42d4bd5be1a6fed7c16be Mon Sep 17 00:00:00 2001 From: phylax2020 Date: Tue, 19 Jul 2022 20:47:11 +0200 Subject: [PATCH 07/48] log_file_controller added again --- cbpi/controller/log_file_controller.py | 224 +++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 cbpi/controller/log_file_controller.py diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py new file mode 100644 index 0000000..968929f --- /dev/null +++ b/cbpi/controller/log_file_controller.py @@ -0,0 +1,224 @@ +import datetime +import glob +import logging +import os +from logging.handlers import RotatingFileHandler +from time import strftime, localtime +import pandas as pd +import zipfile +import base64 +import urllib3 +from cbpi.api import * +from cbpi.api.config import ConfigType +from cbpi.api.base import CBPiBase +import asyncio + + +class LogController: + + def __init__(self, cbpi): + ''' + + :param cbpi: craftbeerpi object + ''' + self.cbpi = cbpi + self.logger = logging.getLogger(__name__) + self.configuration = False + self.datalogger = {} + + def log_data(self, name: str, value: str) -> None: + self.logfiles = self.cbpi.config.get("CSVLOGFILES", "Yes") + self.influxdb = self.cbpi.config.get("INFLUXDB", "No") + if self.logfiles == "Yes": + if name not in self.datalogger: + max_bytes = int(self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 100000)) + backup_count = int(self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3)) + + data_logger = logging.getLogger('cbpi.sensor.%s' % name) + data_logger.propagate = False + data_logger.setLevel(logging.DEBUG) + handler = RotatingFileHandler('./logs/sensor_%s.log' % name, maxBytes=max_bytes, backupCount=backup_count) + data_logger.addHandler(handler) + self.datalogger[name] = data_logger + + formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime()) + self.datalogger[name].info("%s,%s" % (formatted_time, str(value))) + if self.influxdb == "Yes": + self.influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", "No") + self.influxdbaddr = self.cbpi.config.get("INFLUXDBADDR", None) + self.influxdbport = self.cbpi.config.get("INFLUXDBPORT", None) + self.influxdbname = self.cbpi.config.get("INFLUXDBNAME", None) + self.influxdbuser = self.cbpi.config.get("INFLUXDBUSER", None) + self.influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None) + + id = name + try: + chars = {'ö':'oe','ä':'ae','ü':'ue','Ö':'Oe','Ä':'Ae','Ü':'Ue'} + sensor=self.cbpi.sensor.find_by_id(name) + if sensor is not None: + itemname=sensor.name.replace(" ", "_") + for char in chars: + itemname = itemname.replace(char,chars[char]) + out="measurement,source=" + itemname + ",itemID=" + str(id) + " value="+str(value) + except Exception as e: + logging.error("InfluxDB ID Error: {}".format(e)) + + if self.influxdbcloud == "Yes": + self.influxdburl="https://" + self.influxdbaddr + "/api/v2/write?org=" + self.influxdbuser + "&bucket=" + self.influxdbname + "&precision=s" + try: + header = {'User-Agent': name, 'Authorization': "Token {}".format(self.influxdbpwd)} + http = urllib3.PoolManager() + req = http.request('POST',self.influxdburl, body=out, headers = header) + except Exception as e: + logging.error("InfluxDB cloud write Error: {}".format(e)) + + else: + self.base64string = base64.b64encode(('%s:%s' % (self.influxdbuser,self.influxdbpwd)).encode()) + self.influxdburl='http://' + self.influxdbaddr + ':' + str(self.influxdbport) + '/write?db=' + self.influxdbname + try: + header = {'User-Agent': name, 'Content-Type': 'application/x-www-form-urlencoded','Authorization': 'Basic %s' % self.base64string.decode('utf-8')} + http = urllib3.PoolManager() + req = http.request('POST',self.influxdburl, body=out, headers = header) + except Exception as e: + logging.error("InfluxDB write Error: {}".format(e)) + + + + async def get_data(self, names, sample_rate='60s'): + logging.info("Start Log for {}".format(names)) + ''' + :param names: name as string or list of names as string + :param sample_rate: rate for resampling the data + :return: + ''' + # make string to array + if isinstance(names, list) is False: + names = [names] + + # remove duplicates + names = set(names) + + + result = None + + def dateparse(time_in_secs): + ''' + Internal helper for date parsing + :param time_in_secs: + :return: + ''' + return datetime.datetime.strptime(time_in_secs, '%Y-%m-%d %H:%M:%S') + + def datetime_to_str(o): + if isinstance(o, datetime.datetime): + return o.__str__() + + for name in names: + # get all log names + all_filenames = glob.glob('./logs/sensor_%s.log*' % name) + # concat all logs + df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', name], header=None) for f in all_filenames]) + logging.info("Read all files for {}".format(names)) + # resample if rate provided + if sample_rate is not None: + df = df[name].resample(sample_rate).max() + logging.info("Sampled now for {}".format(names)) + df = df.dropna() + # take every nth row so that total number of rows does not exceed max_rows * 2 + max_rows = 500 + total_rows = df.shape[0] + if (total_rows > 0) and (total_rows > max_rows): + nth = int(total_rows/max_rows) + if nth > 1: + df = df.iloc[::nth] + + if result is None: + result = df + else: + result = pd.merge(result, df, how='outer', left_index=True, right_index=True) + + data = {"time": df.index.tolist()} + + if len(names) > 1: + for name in names: + data[name] = result[name].interpolate(limit_direction='both', limit=10).tolist() + else: + data[name] = result.interpolate().tolist() + + logging.info("Send Log for {}".format(names)) + + return data + + async def get_data2(self, ids) -> dict: + def dateparse(time_in_secs): + return datetime.datetime.strptime(time_in_secs, '%Y-%m-%d %H:%M:%S') + + result = dict() + for id in ids: + # df = pd.read_csv("./logs/sensor_%s.log" % id, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime',"Values"], header=None) + # concat all logs + all_filenames = glob.glob('./logs/sensor_%s.log*' % id) + df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Values'], header=None) for f in all_filenames]) + df = df.resample('60s').max() + df = df.dropna() + result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()} + return result + + + + def get_logfile_names(self, name:str ) -> list: + ''' + Get all log file names + :param name: log name as string. pattern /logs/sensor_%s.log* + :return: list of log file names + ''' + + return [os.path.basename(x) for x in glob.glob('./logs/sensor_%s.log*' % name)] + + def clear_log(self, name:str ) -> str: + + all_filenames = glob.glob('./logs/sensor_%s.log*' % name) + for f in all_filenames: + os.remove(f) + + if name in self.datalogger: + del self.datalogger[name] + + + def get_all_zip_file_names(self, name: str) -> list: + + ''' + Return a list of all zip file names + :param name: + :return: + ''' + + return [os.path.basename(x) for x in glob.glob('./logs/*-sensor-%s.zip' % name)] + + def clear_zip(self, name:str ) -> None: + """ + clear all zip files for a sensor + :param name: sensor name + :return: None + """ + + all_filenames = glob.glob('./logs/*-sensor-%s.zip' % name) + for f in all_filenames: + os.remove(f) + + def zip_log_data(self, name: str) -> str: + """ + :param name: sensor name + :return: zip_file_name + """ + + formatted_time = strftime("%Y-%m-%d-%H_%M_%S", localtime()) + file_name = './logs/%s-sensor-%s.zip' % (formatted_time, name) + zip = zipfile.ZipFile(file_name, 'w', zipfile.ZIP_DEFLATED) + all_filenames = glob.glob('./logs/sensor_%s.log*' % name) + for f in all_filenames: + zip.write(os.path.join(f)) + zip.close() + return os.path.basename(file_name) + + From 16190124d0e608baec14b44a98696b7721a39f5f Mon Sep 17 00:00:00 2001 From: phylax2020 Date: Tue, 19 Jul 2022 21:03:17 +0200 Subject: [PATCH 08/48] hopefully this commit works! --- cbpi/controller/log_file_controller.py | 27 ++++++++++++++++--------- cbpi/extension/ConfigUpdate/__init__.py | 3 ++- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py index bf10cb2..968929f 100644 --- a/cbpi/controller/log_file_controller.py +++ b/cbpi/controller/log_file_controller.py @@ -31,13 +31,8 @@ class LogController: self.influxdb = self.cbpi.config.get("INFLUXDB", "No") if self.logfiles == "Yes": if name not in self.datalogger: -<<<<<<< HEAD max_bytes = int(self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 100000)) backup_count = int(self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3)) -======= - max_bytes = self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 1048576) - backup_count = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3) ->>>>>>> 5ea4160b4f6b7a7c759323cc01b0e40a05bca4b9 data_logger = logging.getLogger('cbpi.sensor.%s' % name) data_logger.propagate = False @@ -47,7 +42,7 @@ class LogController: self.datalogger[name] = data_logger formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime()) - self.datalogger[name].info("%s,%s" % (formatted_time, value)) + self.datalogger[name].info("%s,%s" % (formatted_time, str(value))) if self.influxdb == "Yes": self.influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", "No") self.influxdbaddr = self.cbpi.config.get("INFLUXDBADDR", None) @@ -121,7 +116,6 @@ class LogController: for name in names: # get all log names all_filenames = glob.glob('./logs/sensor_%s.log*' % name) - # concat all logs df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', name], header=None) for f in all_filenames]) logging.info("Read all files for {}".format(names)) @@ -130,19 +124,29 @@ class LogController: df = df[name].resample(sample_rate).max() logging.info("Sampled now for {}".format(names)) df = df.dropna() + # take every nth row so that total number of rows does not exceed max_rows * 2 + max_rows = 500 + total_rows = df.shape[0] + if (total_rows > 0) and (total_rows > max_rows): + nth = int(total_rows/max_rows) + if nth > 1: + df = df.iloc[::nth] + if result is None: result = df else: result = pd.merge(result, df, how='outer', left_index=True, right_index=True) data = {"time": df.index.tolist()} - + if len(names) > 1: for name in names: data[name] = result[name].interpolate(limit_direction='both', limit=10).tolist() else: data[name] = result.interpolate().tolist() + logging.info("Send Log for {}".format(names)) + return data async def get_data2(self, ids) -> dict: @@ -151,7 +155,12 @@ class LogController: result = dict() for id in ids: - df = pd.read_csv("./logs/sensor_%s.log" % id, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime',"Values"], header=None) + # df = pd.read_csv("./logs/sensor_%s.log" % id, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime',"Values"], header=None) + # concat all logs + all_filenames = glob.glob('./logs/sensor_%s.log*' % id) + df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Values'], header=None) for f in all_filenames]) + df = df.resample('60s').max() + df = df.dropna() result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()} return result diff --git a/cbpi/extension/ConfigUpdate/__init__.py b/cbpi/extension/ConfigUpdate/__init__.py index 8380e75..67ac417 100644 --- a/cbpi/extension/ConfigUpdate/__init__.py +++ b/cbpi/extension/ConfigUpdate/__init__.py @@ -47,7 +47,8 @@ class ConfigUpdate(CBPiExtension): influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None) mqttupdate = self.cbpi.config.get("MQTTUpdate", None) PRESSURE_UNIT = self.cbpi.config.get("PRESSURE_UNIT", None) - + SENSOR_LOG_BACKUP_COUNT = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", None) + SENSOR_LOG_MAX_BYTES = self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", None) if boil_temp is None: logger.info("INIT Boil Temp Setting") From c0997ff357ecfe3b0fc0c8ececd125091b207aae Mon Sep 17 00:00:00 2001 From: phylax2020 Date: Thu, 21 Jul 2022 10:27:54 +0200 Subject: [PATCH 09/48] Add setting parameter for dashboard pipe animation slow down --- cbpi/controller/dashboard_controller.py | 4 ++++ cbpi/extension/ConfigUpdate/__init__.py | 16 +++++++++++++--- cbpi/http_endpoints/http_dashboard.py | 12 ++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/cbpi/controller/dashboard_controller.py b/cbpi/controller/dashboard_controller.py index 228839c..2f39af5 100644 --- a/cbpi/controller/dashboard_controller.py +++ b/cbpi/controller/dashboard_controller.py @@ -64,3 +64,7 @@ class DashboardController: async def set_current_dashboard(self, dashboard_id=1): await self.cbpi.config.set("current_dashboard_number", dashboard_id) return {"status": "OK"} + + async def get_slow_pipe_animation(self): + slow_pipe_animation = self.cbpi.config.get("slow_pipe_animation", "Yes") + return slow_pipe_animation \ No newline at end of file diff --git a/cbpi/extension/ConfigUpdate/__init__.py b/cbpi/extension/ConfigUpdate/__init__.py index 67ac417..80808c3 100644 --- a/cbpi/extension/ConfigUpdate/__init__.py +++ b/cbpi/extension/ConfigUpdate/__init__.py @@ -49,7 +49,8 @@ class ConfigUpdate(CBPiExtension): PRESSURE_UNIT = self.cbpi.config.get("PRESSURE_UNIT", None) SENSOR_LOG_BACKUP_COUNT = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", None) SENSOR_LOG_MAX_BYTES = self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", None) - + slow_pipe_animation = self.cbpi.config.get("slow_pipe_animation", None) + if boil_temp is None: logger.info("INIT Boil Temp Setting") try: @@ -299,11 +300,20 @@ class ConfigUpdate(CBPiExtension): if SENSOR_LOG_MAX_BYTES is None: logger.info("Init maximum size of sensor logfiles") try: - await self.cbpi.config.add("SENSOR_LOG_MAX_BYTES", 100000, ConfigType.NUMBER, "Max. number of bytes in sensor logfiles") + await self.cbpi.config.add("SENSOR_LOG_MAX_BYTES", 100000, ConfigType.NUMBER, "Max. number of bytes in sensor logs") except: logger.warning('Unable to update database') - + # Check if slow_pipe_animation is in config + if slow_pipe_animation is None: + logger.info("INIT slow_pipe_animation") + try: + await self.cbpi.config.add("slow_pipe_animation", "Yes", ConfigType.SELECT, "Slow down dashboard pipe animation taking up close to 100% of the CPU's capacity", + [{"label": "Yes", "value": "Yes"}, + {"label": "No", "value": "No"}]) + except: + logger.warning('Unable to update config') + def setup(cbpi): cbpi.plugin.register("ConfigUpdate", ConfigUpdate) pass diff --git a/cbpi/http_endpoints/http_dashboard.py b/cbpi/http_endpoints/http_dashboard.py index 53d5d8a..f24fe0c 100644 --- a/cbpi/http_endpoints/http_dashboard.py +++ b/cbpi/http_endpoints/http_dashboard.py @@ -157,3 +157,15 @@ class DashBoardHttpEndpoints: dashboard_id = int(request.match_info['id']) return web.json_response(await self.cbpi.dashboard.set_current_dashboard(dashboard_id)) + @request_mapping(path="/slowPipeAnimation", method="GET", auth_required=False) + async def get_slow_pipe_animation(self, request): + """ + --- + description: Get slow down dashboard pipe animation (Yes/No) + tags: + - Dashboard + responses: + "200": + description: successful operation + """ + return web.json_response(await self.cbpi.dashboard.get_slow_pipe_animation(), dumps=json_dumps) From c7d01c33f65487ee3fabb604e6c3ae9fb2205f7f Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 14 Aug 2022 12:42:29 +0200 Subject: [PATCH 10/48] change version # for pipe animation speed --- cbpi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index d8703f2..6bdbf74 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a1" +__version__ = "4.0.7.a2" __codename__ = "Spring Break" From 9c6ede13f1712ec7c75deb3db831fb31c1f564dc Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 14 Aug 2022 13:39:08 +0200 Subject: [PATCH 11/48] change upload controller to support kbh 2.4.0 hop timer categories --- cbpi/__init__.py | 2 +- cbpi/controller/upload_controller.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 6bdbf74..f857583 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a2" +__version__ = "4.0.7.a3" __codename__ = "Spring Break" diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index 2d21de0..f0a32c2 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -193,7 +193,7 @@ class UploadController: pass # get the hop addition times - c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze = 0 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze <> 1 AND Vorderwuerze <> 5 AND SudID = ?', (Recipe_ID,)) hops = c.fetchall() # get the misc addition times From ac3c88052307a09bcbf816925a636719bbbe8046 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Wed, 31 Aug 2022 20:46:27 +0200 Subject: [PATCH 12/48] actor update api for WS --- cbpi/__init__.py | 2 +- cbpi/controller/actor_controller.py | 8 ++++++++ cbpi/http_endpoints/http_actor.py | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index f857583..d49370d 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a3" +__version__ = "4.0.7.a4" __codename__ = "Spring Break" diff --git a/cbpi/controller/actor_controller.py b/cbpi/controller/actor_controller.py index 1ae2a01..ae9ba9d 100644 --- a/cbpi/controller/actor_controller.py +++ b/cbpi/controller/actor_controller.py @@ -66,3 +66,11 @@ class ActorController(BasicController): self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict()) except Exception as e: logging.error("Failed to update Actor {} {}".format(id, e)) + + async def ws_actor_update(self): + try: + #await self.push_udpate() + self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda x: x.to_dict(), self.data))),self.sorting) +# self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict()) + except Exception as e: + logging.error("Failed to update Actors {}".format(e)) \ No newline at end of file diff --git a/cbpi/http_endpoints/http_actor.py b/cbpi/http_endpoints/http_actor.py index 8a6ea3b..7738a5a 100644 --- a/cbpi/http_endpoints/http_actor.py +++ b/cbpi/http_endpoints/http_actor.py @@ -24,6 +24,21 @@ class ActorHttpEndpoints(): """ return web.json_response(data=self.controller.get_state()) + @request_mapping(path="/ws_update", auth_required=False) + async def http_get_all(self, request): + """ + + --- + description: Update WS actors + tags: + - Actor + responses: + "204": + description: successful operation + """ + return web.json_response(data=await self.controller.ws_actor_update()) + + @request_mapping(path="/{id:\w+}", auth_required=False) async def http_get_one(self, request): """ From 2f085965c780c5baa9a475f7ae1aa668320828df Mon Sep 17 00:00:00 2001 From: prash3r Date: Fri, 2 Sep 2022 17:00:50 +0200 Subject: [PATCH 13/48] extends cbpi-dev-config to be a complete set. cbpi requires empty folders inside the config folder to fully function, and this commit adds them. There are also some more extensive checks on missing files and folders newly implemented. As well as checking for restored_config.zip is now done before checking for config.yaml. On unsuccessfull restore the zip file is renamed instead of deleted. --- .../{_widgets_are_placed_here => .gitkeep} | 0 .../.gitkeep} | 0 .../cbpi-dev-config/logs/sensors/.gitkeep | 0 .../cbpi-dev-config/recipes/.gitkeep | 0 .devcontainer/cbpi-dev-config/upload/.gitkeep | 0 .gitignore | 3 +- .vscode/launch.json | 10 +- cbpi/configFolder.py | 114 +++++++++++++----- 8 files changed, 97 insertions(+), 30 deletions(-) rename .devcontainer/cbpi-dev-config/dashboard/widgets/{_widgets_are_placed_here => .gitkeep} (100%) rename .devcontainer/cbpi-dev-config/{upload/_uploads_are_placed_here => fermenterrecipes/.gitkeep} (100%) create mode 100644 .devcontainer/cbpi-dev-config/logs/sensors/.gitkeep create mode 100644 .devcontainer/cbpi-dev-config/recipes/.gitkeep create mode 100644 .devcontainer/cbpi-dev-config/upload/.gitkeep diff --git a/.devcontainer/cbpi-dev-config/dashboard/widgets/_widgets_are_placed_here b/.devcontainer/cbpi-dev-config/dashboard/widgets/.gitkeep similarity index 100% rename from .devcontainer/cbpi-dev-config/dashboard/widgets/_widgets_are_placed_here rename to .devcontainer/cbpi-dev-config/dashboard/widgets/.gitkeep diff --git a/.devcontainer/cbpi-dev-config/upload/_uploads_are_placed_here b/.devcontainer/cbpi-dev-config/fermenterrecipes/.gitkeep similarity index 100% rename from .devcontainer/cbpi-dev-config/upload/_uploads_are_placed_here rename to .devcontainer/cbpi-dev-config/fermenterrecipes/.gitkeep diff --git a/.devcontainer/cbpi-dev-config/logs/sensors/.gitkeep b/.devcontainer/cbpi-dev-config/logs/sensors/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.devcontainer/cbpi-dev-config/recipes/.gitkeep b/.devcontainer/cbpi-dev-config/recipes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.devcontainer/cbpi-dev-config/upload/.gitkeep b/.devcontainer/cbpi-dev-config/upload/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore index bb723ac..20ad518 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ node_modules .DS_Store config/* logs/ -.coverage \ No newline at end of file +.coverage +.devcontainer/cbpi-dev-config/* \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index c2f9637..feff4f1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,15 @@ "configurations": [ { - "name": "Run CraftBeerPi4", + "name": "setup CraftBeerPi4: create config folder structure", + "type": "python", + "request": "launch", + "module": "run", + "args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "setup"] + }, + + { + "name": "run CraftBeerPi4", "type": "python", "request": "launch", "module": "run", diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index 5741d4f..8926a91 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -1,3 +1,4 @@ +from ast import If, Try import os from os import listdir from os.path import isfile, join @@ -33,25 +34,8 @@ class ConfigFolder: return fermenter_recipe_ids def check_for_setup(self): - if self.config_file_exists("config.yaml") is False: - print("***************************************************") - print("CraftBeerPi Config File not found: %s" % self.get_file_path("config.yaml")) - print("Please run 'cbpi setup' before starting the server ") - print("***************************************************") - return False - if self.config_file_exists("upload") is False: - print("***************************************************") - print("CraftBeerPi upload folder not found: %s" % self.get_file_path("upload")) - print("Please run 'cbpi setup' before starting the server ") - print("***************************************************") - return False - # if os.path.exists(os.path.join(".", "config", "fermenterrecipes")) is False: - # print("***************************************************") - # print("CraftBeerPi fermenterrecipes folder not found: %s" % os.path.join(".", "config/fermenterrecipes")) - # print("Please run 'cbpi setup' before starting the server ") - # print("***************************************************") - # return False - backupfile = os.path.join(".", "restored_config.zip") + # is there a restored_config.zip file? if yes restore it first then delte the zip. + backupfile = os.path.join(self._rawPath, "restored_config.zip") if os.path.exists(os.path.join(backupfile)) is True: print("***************************************************") print("Found backup of config. Starting restore") @@ -75,7 +59,7 @@ class ConfigFolder: owner = output_path.owner() group = output_path.group() print("Removing old config folder") - shutil.rmtree(output_path, ignore_errors=True) + shutil.rmtree(output_path, ignore_errors=True) print("Extracting zip file to config folder") zip.extractall(output_path) zip.close() @@ -83,12 +67,79 @@ class ConfigFolder: print(f"Changing owner and group of config folder recursively to {owner}:{group}") self.recursive_chown(output_path, owner, group) print("Removing backup file") - os.remove(backupfile) + print("contents of restored_config.zip file have been restored.") + print("in case of a partial backup you will still be prompted to run 'cbpi setup'.") + # os.remove(backupfile) # since the zip was inside the config folder and the config folder was deleted 10 lines ago this file doesnt exist anymore else: print("Wrong Content in zip file. No restore possible") - print("Removing zip file") - os.remove(backupfile) + print("renaming zip file so it will be ignored on the next start") + try: + os.rename(backupfile, os.path.join(self._rawPath, "UNRESTORABLE_restored_config.zip")) + except: + print("renamed file does exist - deleting instead") + os.remove(backupfile) print("***************************************************") + # possible restored_config.zip has been handeled now lets check if files and folders exist + required_config_content = [ + ['config.yaml', 'file'], + ['actor.json', 'file'], + ['sensor.json', 'file'], + ['kettle.json', 'file'], + ['fermenter_data.json', 'file'], + ['step_data.json', 'file'], + ['config.json', 'file'], + ['craftbeerpi.service', 'file'], + ['chromium.desktop', 'file'], + ['dashboard/cbpi_dashboard_1.json', 'file'], + ['dashboard', 'folder'], + ['dashboard/widgets', 'folder'], + ['fermenterrecipes', 'folder'], + ['logs', 'folder'], + ['logs/sensors', 'folder'], + ['recipes', 'folder'], + ['upload', 'folder'] + ] + for checking in required_config_content: + if self.inform_missing_content(self.check_for_file_or_folder(os.path.join(self._rawPath, checking[0]), checking[1])): + # since there is no complete config we now check if the config folde rmay be completely empty to show hints: + if len(os.listdir(os.path.join(self._rawPath))) == 0 : + print("***************************************************") + print(f"the config folder '{self._rawPath}' seems to be completely empty") + print("you might want to run 'cbpi setup'.print") + print("but you could also place your zipped config backup named") + print("'restored_config.zip' inside the mentioned config folder for") + print("cbpi4 to automatically unpack it") + print("of course you can also place your config files manually") + print("***************************************************") + return False + + def inform_missing_content(self, whatsmissing : str): + if whatsmissing == "": + return False + print("***************************************************") + print(f"CraftBeerPi config content not found: {whatsmissing}") + print("Please run 'cbpi setup' before starting the server ") + print("***************************************************") + return True + + + def check_for_file_or_folder(self, path : str, file_or_folder : str = ""): # file_or_folder should be "file" or "folder" or "" if both is ok + if (file_or_folder == ""): # file and folder is ok + if os.path.exists(path): + return "" + else: + return "file or folder missing: " + path + if (file_or_folder == "file"): # only file is ok + if (os.path.isfile(path)): + return "" + else: + return "file missing: " + path + if (file_or_folder == "folder"): # oly folder is ok + if (os.path.isdir(path)): + return "" + else: + return "folder missing: " + path + return "usage of check_file_or_folder() function wrong. second Argument must either be 'file' or 'folder' or an empty string" def copyDefaultFileIfNotExists(self, file): if self.config_file_exists(file) is False: @@ -115,7 +166,7 @@ class ConfigFolder: print("Config Folder created") def create_home_folder_structure(configFolder): - pathlib.Path(os.path.join(".", 'logs/sensors')).mkdir(parents=True, exist_ok=True) + # pathlib.Path(os.path.join(".", 'logs/sensors')).mkdir(parents=True, exist_ok=True) configFolder.create_folders() print("Folder created") @@ -123,11 +174,18 @@ class ConfigFolder: def create_folders(self): pathlib.Path(self._rawPath).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self._rawPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True) + pathlib.Path(os.path.join(self._rawPath, 'logs', 'sensors')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self._rawPath, 'recipes')).mkdir(parents=True, exist_ok=True) + pathlib.Path(os.path.join(self._rawPath, 'fermenterrecipes')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self._rawPath, 'upload')).mkdir(parents=True, exist_ok=True) def recursive_chown(self, path, owner, group): - for dirpath, dirnames, filenames in os.walk(path): - shutil.chown(dirpath, owner, group) - for filename in filenames: - shutil.chown(os.path.join(dirpath, filename), owner, group) \ No newline at end of file + try: + for dirpath, dirnames, filenames in os.walk(path): + shutil.chown(dirpath, owner, group) + for filename in filenames: + shutil.chown(os.path.join(dirpath, filename), owner, group) + except: + print("problems assigning file or folder permissions") + print("if this happend on windows its fine") + print("if this happend in the dev container running inside windows its also fine but you might have to rebuild the container if you run into further problems") From 76b11a7247ebf5c0ec37d56675a31ed0423a9bc6 Mon Sep 17 00:00:00 2001 From: prash3r Date: Fri, 2 Sep 2022 18:17:08 +0200 Subject: [PATCH 14/48] =?UTF-8?q?imports=20the=20whirlpool=20hop=20additio?= =?UTF-8?q?n=20kbh=20with=20timer=20previously=20the=20whirlpool=20step=20?= =?UTF-8?q?has=20been=20ignored=20when=20importing=20from=20a=20kbh=20data?= =?UTF-8?q?base.=20on=20creation=20the=20whirlpoolstep=20would=20only=20be?= =?UTF-8?q?=20on=20a=20hardcoded=2015=20minute=20timer.=20I=20added=20the?= =?UTF-8?q?=20optional=20timer=20as=20argument=20to=20create=5FWhirlpool?= =?UTF-8?q?=5FCooldown().=20And=20made=20use=20of=20it=20for=20negative=20?= =?UTF-8?q?hops=20timer=20imported=20from=20KBH.=20For=20KBH=20databases?= =?UTF-8?q?=20this=20is=20valid=20because=20if=20the=20hops=20timer=20valu?= =?UTF-8?q?e=20is=20negative=20it=20can=20only=20be=20the=20whirlpool=20ti?= =?UTF-8?q?mer=20and=20the=20value=20is=20the=20time=20that=20is=20set=20f?= =?UTF-8?q?or=20the=20brewing=20device=20for=20getting=20from=20boil=20to?= =?UTF-8?q?=20below=2080=C2=B0C.=20I=20did=20only=20test=20the=20data=20an?= =?UTF-8?q?d=20not=20the=20functionality,=20but=20it=20sucessfully=20repla?= =?UTF-8?q?ces=20the=20hardcoded=20"15"=20timer=20with=20the=20value=20fro?= =?UTF-8?q?m=20the=20first=20whirlpool=20hop=20addition=20while=20getting?= =?UTF-8?q?=20rid=20of=20the=20sign.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cbpi/controller/upload_controller.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index f0a32c2..2379faf 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -193,8 +193,19 @@ class UploadController: pass # get the hop addition times - c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze <> 1 AND Vorderwuerze <> 5 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze <> 1 AND SudID = ?', (Recipe_ID,)) hops = c.fetchall() + whirlpool = [] + for hop in hops: + if hop[0] < 0: + whirlpool.append(hop) + for whirl in whirlpool: + hops.remove(whirl) + + print(whirlpool) + print(hops) + + # get the misc addition times c.execute('SELECT Zugabedauer FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) @@ -307,7 +318,7 @@ class UploadController: await self.create_step(step_string) - await self.create_Whirlpool_Cooldown() + await self.create_Whirlpool_Cooldown(str(abs(whirlpool[0][0]))) # from kbh this value comes as negative but must be positive self.cbpi.notify('KBH Recipe created', name, NotificationType.INFO) @@ -946,13 +957,13 @@ class UploadController: alert="Yes" return alert - async def create_Whirlpool_Cooldown(self): + async def create_Whirlpool_Cooldown(self, time : str = "15"): # Add Waitstep as Whirlpool if self.cooldown != "WaiStep" and self.cooldown !="": step_string = { "name": "Whirlpool", "props": { "Kettle": self.boilid, - "Timer": "15" + "Timer": time }, "status_text": "", "status": "I", @@ -965,7 +976,7 @@ class UploadController: step_name = "CoolDown" cooldown_sensor = "" step_temp = "" - step_timer = "15" + step_timer = time if step_type == "CooldownStep": cooldown_sensor = self.cbpi.config.get("steps_cooldown_sensor", None) if cooldown_sensor is None or cooldown_sensor == '': From 2076c66eb5b3d926b5af340afceed0e8955bc40b Mon Sep 17 00:00:00 2001 From: prash3r Date: Sat, 3 Sep 2022 13:43:17 +0200 Subject: [PATCH 15/48] uploaded restored_config.zip is now placed in provided config folder and not the working directory where it will be ignored on restart --- cbpi/controller/system_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cbpi/controller/system_controller.py b/cbpi/controller/system_controller.py index 8c211fc..24128fe 100644 --- a/cbpi/controller/system_controller.py +++ b/cbpi/controller/system_controller.py @@ -116,8 +116,8 @@ class SystemController: try: content = backup_file.read() if backup_file and self.allowed_file(filename, 'zip'): - self.path = os.path.join(".", "restored_config.zip") - + self.path = os.path.join(self.cbpi.config_folder._rawPath, "restored_config.zip") + f=open(self.path, "wb") f.write(content) f.close() From e61fbdb69cb5290295be937117b07819f18e17c2 Mon Sep 17 00:00:00 2001 From: prash3r Date: Wed, 7 Sep 2022 12:39:52 +0200 Subject: [PATCH 16/48] extends #62 reorders the .vscode tasks so run is the default hopefully. removes code that was commented out anyway. --- .vscode/launch.json | 16 ++++++++-------- cbpi/configFolder.py | 3 --- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index feff4f1..73c363b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,20 +5,20 @@ "version": "0.2.0", "configurations": [ - { - "name": "setup CraftBeerPi4: create config folder structure", - "type": "python", - "request": "launch", - "module": "run", - "args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "setup"] - }, - { "name": "run CraftBeerPi4", "type": "python", "request": "launch", "module": "run", "args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "start"] + }, + + { + "name": "setup CraftBeerPi4: create config folder structure", + "type": "python", + "request": "launch", + "module": "run", + "args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "setup"] } ] } \ No newline at end of file diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index 8926a91..058271c 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -69,7 +69,6 @@ class ConfigFolder: print("Removing backup file") print("contents of restored_config.zip file have been restored.") print("in case of a partial backup you will still be prompted to run 'cbpi setup'.") - # os.remove(backupfile) # since the zip was inside the config folder and the config folder was deleted 10 lines ago this file doesnt exist anymore else: print("Wrong Content in zip file. No restore possible") print("renaming zip file so it will be ignored on the next start") @@ -166,8 +165,6 @@ class ConfigFolder: print("Config Folder created") def create_home_folder_structure(configFolder): - # pathlib.Path(os.path.join(".", 'logs/sensors')).mkdir(parents=True, exist_ok=True) - configFolder.create_folders() print("Folder created") From 4952861a584224311c4072c78ce15abca8913022 Mon Sep 17 00:00:00 2001 From: prash3r Date: Fri, 9 Sep 2022 18:38:08 +0200 Subject: [PATCH 17/48] logs folder is next to the active config folder. It was wherever 'cbpi setup' was run from. and if no config folder path is provided its still the same. This also adds a file based global logger placed in the logs folder. The sensor loggers respect the new logs folder location. --- .../cbpi-dev-config/logs/sensors/.gitkeep | 0 cbpi/cli.py | 12 ++++++++++- cbpi/configFolder.py | 7 +++---- cbpi/controller/config_controller.py | 2 ++ cbpi/controller/log_file_controller.py | 21 +++++++++++-------- cbpi/craftbeerpi.py | 6 ------ 6 files changed, 28 insertions(+), 20 deletions(-) delete mode 100644 .devcontainer/cbpi-dev-config/logs/sensors/.gitkeep diff --git a/.devcontainer/cbpi-dev-config/logs/sensors/.gitkeep b/.devcontainer/cbpi-dev-config/logs/sensors/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/cbpi/cli.py b/cbpi/cli.py index c1ff9be..1bf8012 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -1,4 +1,5 @@ import logging +from pathlib import Path import requests from cbpi.configFolder import ConfigFolder from cbpi.utils.utils import load_config @@ -230,7 +231,16 @@ def main(context, config_folder_path): print("Welcome to CBPi") print("---------------------") level = logging.INFO - logging.basicConfig(level=level, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') + logger = logging.getLogger() + logger.setLevel(level) + try: + logger.addHandler(logging.handlers.RotatingFileHandler(os.path.join(Path(config_folder_path).parent, 'logs', f"cbpi.log"), maxBytes=1000000, backupCount=3)) + except: + print("there seems to be no log folder - continueing without (maybe you should run 'cbpi setup')") + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s') + for handler in logger.handlers: + handler.setLevel(level) + handler.setFormatter(formatter) cbpi_cli = CraftBeerPiCli(ConfigFolder(config_folder_path)) context.obj = cbpi_cli pass diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index 058271c..a29db84 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -90,11 +90,10 @@ class ConfigFolder: ['craftbeerpi.service', 'file'], ['chromium.desktop', 'file'], ['dashboard/cbpi_dashboard_1.json', 'file'], - ['dashboard', 'folder'], ['dashboard/widgets', 'folder'], + ['dashboard', 'folder'], ['fermenterrecipes', 'folder'], - ['logs', 'folder'], - ['logs/sensors', 'folder'], + ['../logs', 'folder'], ['recipes', 'folder'], ['upload', 'folder'] ] @@ -171,7 +170,7 @@ class ConfigFolder: def create_folders(self): pathlib.Path(self._rawPath).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self._rawPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True) - pathlib.Path(os.path.join(self._rawPath, 'logs', 'sensors')).mkdir(parents=True, exist_ok=True) + pathlib.Path(os.path.join(self._rawPath, '..','logs')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self._rawPath, 'recipes')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self._rawPath, 'fermenterrecipes')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self._rawPath, 'upload')).mkdir(parents=True, exist_ok=True) diff --git a/cbpi/controller/config_controller.py b/cbpi/controller/config_controller.py index 4afaa0b..2412688 100644 --- a/cbpi/controller/config_controller.py +++ b/cbpi/controller/config_controller.py @@ -1,6 +1,7 @@ from cbpi.api.dataclasses import Config import logging import os +from pathlib import Path from cbpi.api.config import ConfigType from cbpi.utils import load_config @@ -16,6 +17,7 @@ class ConfigController: self.cbpi.register(self) self.path = cbpi.config_folder.get_file_path("config.json") self.path_static = cbpi.config_folder.get_file_path("config.yaml") + self.logger.info("Config folder path : " + os.path.join(Path(self.cbpi.config_folder._rawPath).absolute())) def get_state(self): diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py index 968929f..05024bb 100644 --- a/cbpi/controller/log_file_controller.py +++ b/cbpi/controller/log_file_controller.py @@ -8,6 +8,7 @@ import pandas as pd import zipfile import base64 import urllib3 +from pathlib import Path from cbpi.api import * from cbpi.api.config import ConfigType from cbpi.api.base import CBPiBase @@ -25,6 +26,8 @@ class LogController: self.logger = logging.getLogger(__name__) self.configuration = False self.datalogger = {} + self.logsFolderPath = os.path.join(Path(self.cbpi.config_folder._rawPath).parent.absolute(), 'logs') + self.logger.info("Log folder path : " + self.logsFolderPath) def log_data(self, name: str, value: str) -> None: self.logfiles = self.cbpi.config.get("CSVLOGFILES", "Yes") @@ -37,7 +40,7 @@ class LogController: data_logger = logging.getLogger('cbpi.sensor.%s' % name) data_logger.propagate = False data_logger.setLevel(logging.DEBUG) - handler = RotatingFileHandler('./logs/sensor_%s.log' % name, maxBytes=max_bytes, backupCount=backup_count) + handler = RotatingFileHandler(os.path.join(self.logsFolderPath, f"sensor_{name}.log"), maxBytes=max_bytes, backupCount=backup_count) data_logger.addHandler(handler) self.datalogger[name] = data_logger @@ -115,7 +118,7 @@ class LogController: for name in names: # get all log names - all_filenames = glob.glob('./logs/sensor_%s.log*' % name) + all_filenames = os.path.join(self.logsFolderPath, f"sensor_{name}.log*") # concat all logs df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', name], header=None) for f in all_filenames]) logging.info("Read all files for {}".format(names)) @@ -157,7 +160,7 @@ class LogController: for id in ids: # df = pd.read_csv("./logs/sensor_%s.log" % id, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime',"Values"], header=None) # concat all logs - all_filenames = glob.glob('./logs/sensor_%s.log*' % id) + all_filenames = glob.glob(os.path.join(self.logsFolderPath,f"./logs/sensor_{id}.log*")) df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Values'], header=None) for f in all_filenames]) df = df.resample('60s').max() df = df.dropna() @@ -173,11 +176,11 @@ class LogController: :return: list of log file names ''' - return [os.path.basename(x) for x in glob.glob('./logs/sensor_%s.log*' % name)] + return [os.path.basename(x) for x in os.path.join(self.logsFolderPath, f"sensor_{name}.log*")] def clear_log(self, name:str ) -> str: - all_filenames = glob.glob('./logs/sensor_%s.log*' % name) + all_filenames = os.path.join(self.logsFolderPath, f"sensor_{name}.log*") for f in all_filenames: os.remove(f) @@ -193,7 +196,7 @@ class LogController: :return: ''' - return [os.path.basename(x) for x in glob.glob('./logs/*-sensor-%s.zip' % name)] + return [os.path.basename(x) for x in glob.glob(os.path.join(self.logsFolderPath, f"*-sensor-{name}.zip"))] def clear_zip(self, name:str ) -> None: """ @@ -202,7 +205,7 @@ class LogController: :return: None """ - all_filenames = glob.glob('./logs/*-sensor-%s.zip' % name) + all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"*-sensor-{name}.zip")) for f in all_filenames: os.remove(f) @@ -213,9 +216,9 @@ class LogController: """ formatted_time = strftime("%Y-%m-%d-%H_%M_%S", localtime()) - file_name = './logs/%s-sensor-%s.zip' % (formatted_time, name) + file_name = os.path.join(self.logsFolderPath, f"./logs/{formatted_time}-sensor-{name}.zip" % (formatted_time, name)) zip = zipfile.ZipFile(file_name, 'w', zipfile.ZIP_DEFLATED) - all_filenames = glob.glob('./logs/sensor_%s.log*' % name) + all_filenames = os.path.join(self.logsFolderPath, f"sensor_{name}.log*") for f in all_filenames: zip.write(os.path.join(f)) zip.close() diff --git a/cbpi/craftbeerpi.py b/cbpi/craftbeerpi.py index c4dad9d..d3c60c6 100644 --- a/cbpi/craftbeerpi.py +++ b/cbpi/craftbeerpi.py @@ -301,12 +301,6 @@ class CraftBeerPi: self._swagger_setup() - level = logging.INFO - logger = logging.getLogger() - logger.setLevel(level) - for handler in logger.handlers: - handler.setLevel(level) - return self.app def start(self): From c56d7ccc51b29693914392d01023ecebad0cae14 Mon Sep 17 00:00:00 2001 From: prash3r Date: Mon, 12 Sep 2022 21:54:51 +0200 Subject: [PATCH 18/48] adds --logs-folder-path as cli option --- cbpi/cli.py | 41 +++++++++++++++------- cbpi/configFolder.py | 47 ++++++++++++++------------ cbpi/controller/config_controller.py | 2 +- cbpi/controller/log_file_controller.py | 2 +- cbpi/controller/system_controller.py | 2 +- 5 files changed, 56 insertions(+), 38 deletions(-) diff --git a/cbpi/cli.py b/cbpi/cli.py index 1bf8012..83a2582 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -9,6 +9,7 @@ import os import pkgutil import shutil import click +import pathlib from subprocess import call from colorama import Fore, Back, Style import importlib @@ -16,6 +17,7 @@ from importlib_metadata import metadata from tabulate import tabulate from PyInquirer import prompt, print_json import platform +import subprocess class CraftBeerPiCli(): def __init__(self, config) -> None: @@ -226,24 +228,37 @@ class CraftBeerPiCli(): @click.group() @click.pass_context @click.option('--config-folder-path', '-c', default="./config", type=click.Path(), help="Specify where the config folder is located. Defaults to './config'.") -def main(context, config_folder_path): +@click.option('--logs-folder-path', '-l', default="", type=click.Path(), help="Specify where the log folder is located. Defaults to '../logs' relative from the config folder.") +@click.option('--debug-log-level', '-d', default="30", type=int, help="Specify the log level you want to write to all logs. 0=ALL, 10=DEBUG, 20=INFO 30(default)=WARNING, 40=ERROR, 50=CRITICAL") +def main(context, config_folder_path, logs_folder_path, debug_log_level): print("---------------------") print("Welcome to CBPi") print("---------------------") - level = logging.INFO - logger = logging.getLogger() - logger.setLevel(level) - try: - logger.addHandler(logging.handlers.RotatingFileHandler(os.path.join(Path(config_folder_path).parent, 'logs', f"cbpi.log"), maxBytes=1000000, backupCount=3)) - except: - print("there seems to be no log folder - continueing without (maybe you should run 'cbpi setup')") + if logs_folder_path == "": + logs_folder_path = os.path.join(Path(config_folder_path).absolute().parent, 'logs') formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s') - for handler in logger.handlers: - handler.setLevel(level) - handler.setFormatter(formatter) - cbpi_cli = CraftBeerPiCli(ConfigFolder(config_folder_path)) + logging.basicConfig(format=formatter,level=debug_log_level, stream=logging.StreamHandler()) + logger = logging.getLogger() + try: + result = subprocess.run(['journalctl', '--since', '2 hours ago', '-u', 'craftbeerpi.service' ], stdout=subprocess.PIPE) + # journalctl is present, we assume we are running in production. + # We therefore omit the timestamp from the stdout log handler formatter because timestamps are added to the logs by journalctl anyway + logger.handlers[0].setFormatter('%(levelname)s - %(name)s - %(message)s') + except: + # journalctl command seems to not be present. + # We assume we are in the dev container and keep writing timestampts to stdout for vscode terminal output + logger.warning("journalctl command error - assuming dev container execution and writing timestamps to stdout") + try: + if not os.path.isdir(logs_folder_path): + logger.info(f"logs folder '{logs_folder_path}' doesnt exist and we are trying to create it") + pathlib.Path(logs_folder_path).mkdir(parents=True, exist_ok=True) + logger.info(f"logs folder '{logs_folder_path}' successfully created") + logger.addHandler(logging.handlers.RotatingFileHandler(os.path.join(logs_folder_path, f"cbpi.log"), maxBytes=1000000, backupCount=3)) + except Exception as e: + logger.warning("log folder or log file could not be created or accessed. check folder and file permissions or create the logs folder somewhere you have access with a start option like '--log-folder-path=./logs'") + logging.critical(e, exc_info=True) + cbpi_cli = CraftBeerPiCli(ConfigFolder(config_folder_path, logs_folder_path)) context.obj = cbpi_cli - pass @main.command() @click.pass_context diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index a29db84..d3690cb 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -6,36 +6,39 @@ import pathlib import platform import shutil import zipfile +from pathlib import Path import glob class ConfigFolder: - def __init__(self, configFolderPath): - self._rawPath = configFolderPath + def __init__(self, configFolderPath, logsFolderPath): + self.configFolderPath = configFolderPath + self.logsFolderPath = logsFolderPath + self.logger.info("Config folder path : " + os.path.join(Path(self.cbpi.config_folder.configFolderPath).absolute())) def config_file_exists(self, path): return os.path.exists(self.get_file_path(path)) def get_file_path(self, file): - return os.path.join(self._rawPath, file) + return os.path.join(self.configFolderPath, file) def get_upload_file(self, file): - return os.path.join(self._rawPath, 'upload', file) + return os.path.join(self.configFolderPath, 'upload', file) def get_recipe_file_by_id(self, recipe_id): - return os.path.join(self._rawPath, 'recipes', "{}.yaml".format(recipe_id)) + return os.path.join(self.configFolderPath, 'recipes', "{}.yaml".format(recipe_id)) def get_fermenter_recipe_by_id(self, recipe_id): - return os.path.join(self._rawPath, 'fermenterrecipes', "{}.yaml".format(recipe_id)) + return os.path.join(self.configFolderPath, 'fermenterrecipes', "{}.yaml".format(recipe_id)) def get_all_fermenter_recipes(self): - fermenter_recipes_folder = os.path.join(self._rawPath, 'fermenterrecipes') + fermenter_recipes_folder = os.path.join(self.configFolderPath, 'fermenterrecipes') fermenter_recipe_ids = [os.path.splitext(f)[0] for f in listdir(fermenter_recipes_folder) if isfile(join(fermenter_recipes_folder, f)) and f.endswith(".yaml")] return fermenter_recipe_ids def check_for_setup(self): # is there a restored_config.zip file? if yes restore it first then delte the zip. - backupfile = os.path.join(self._rawPath, "restored_config.zip") + backupfile = os.path.join(self.configFolderPath, "restored_config.zip") if os.path.exists(os.path.join(backupfile)) is True: print("***************************************************") print("Found backup of config. Starting restore") @@ -52,7 +55,7 @@ class ConfigFolder: if zip_content == True: print("Found correct content. Starting Restore process") - output_path = pathlib.Path(self._rawPath) + output_path = pathlib.Path(self.configFolderPath) system = platform.system() print(system) if system != "Windows": @@ -73,7 +76,7 @@ class ConfigFolder: print("Wrong Content in zip file. No restore possible") print("renaming zip file so it will be ignored on the next start") try: - os.rename(backupfile, os.path.join(self._rawPath, "UNRESTORABLE_restored_config.zip")) + os.rename(backupfile, os.path.join(self.configFolderPath, "UNRESTORABLE_restored_config.zip")) except: print("renamed file does exist - deleting instead") os.remove(backupfile) @@ -98,11 +101,11 @@ class ConfigFolder: ['upload', 'folder'] ] for checking in required_config_content: - if self.inform_missing_content(self.check_for_file_or_folder(os.path.join(self._rawPath, checking[0]), checking[1])): + if self.inform_missing_content(self.check_for_file_or_folder(os.path.join(self.configFolderPath, checking[0]), checking[1])): # since there is no complete config we now check if the config folde rmay be completely empty to show hints: - if len(os.listdir(os.path.join(self._rawPath))) == 0 : + if len(os.listdir(os.path.join(self.configFolderPath))) == 0 : print("***************************************************") - print(f"the config folder '{self._rawPath}' seems to be completely empty") + print(f"the config folder '{self.configFolderPath}' seems to be completely empty") print("you might want to run 'cbpi setup'.print") print("but you could also place your zipped config backup named") print("'restored_config.zip' inside the mentioned config folder for") @@ -142,7 +145,7 @@ class ConfigFolder: def copyDefaultFileIfNotExists(self, file): if self.config_file_exists(file) is False: srcfile = os.path.join(os.path.dirname(__file__), "config", file) - destfile = os.path.join(self._rawPath, file) + destfile = os.path.join(self.configFolderPath, file) shutil.copy(srcfile, destfile) def create_config_file(self): @@ -156,9 +159,9 @@ class ConfigFolder: self.copyDefaultFileIfNotExists("craftbeerpi.service") self.copyDefaultFileIfNotExists("chromium.desktop") - if os.path.exists(os.path.join(self._rawPath, "dashboard", "cbpi_dashboard_1.json")) is False: + if os.path.exists(os.path.join(self.configFolderPath, "dashboard", "cbpi_dashboard_1.json")) is False: srcfile = os.path.join(os.path.dirname(__file__), "config", "dashboard", "cbpi_dashboard_1.json") - destfile = os.path.join(self._rawPath, "dashboard") + destfile = os.path.join(self.configFolderPath, "dashboard") shutil.copy(srcfile, destfile) print("Config Folder created") @@ -168,12 +171,12 @@ class ConfigFolder: print("Folder created") def create_folders(self): - pathlib.Path(self._rawPath).mkdir(parents=True, exist_ok=True) - pathlib.Path(os.path.join(self._rawPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True) - pathlib.Path(os.path.join(self._rawPath, '..','logs')).mkdir(parents=True, exist_ok=True) - pathlib.Path(os.path.join(self._rawPath, 'recipes')).mkdir(parents=True, exist_ok=True) - pathlib.Path(os.path.join(self._rawPath, 'fermenterrecipes')).mkdir(parents=True, exist_ok=True) - pathlib.Path(os.path.join(self._rawPath, 'upload')).mkdir(parents=True, exist_ok=True) + pathlib.Path(self.configFolderPath).mkdir(parents=True, exist_ok=True) + pathlib.Path(os.path.join(self.configFolderPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True) + #pathlib.Path(os.path.join(self.configFolderPath, '..','logs')).mkdir(parents=True, exist_ok=True) + pathlib.Path(os.path.join(self.configFolderPath, 'recipes')).mkdir(parents=True, exist_ok=True) + pathlib.Path(os.path.join(self.configFolderPath, 'fermenterrecipes')).mkdir(parents=True, exist_ok=True) + pathlib.Path(os.path.join(self.configFolderPath, 'upload')).mkdir(parents=True, exist_ok=True) def recursive_chown(self, path, owner, group): try: diff --git a/cbpi/controller/config_controller.py b/cbpi/controller/config_controller.py index 2412688..6d62d79 100644 --- a/cbpi/controller/config_controller.py +++ b/cbpi/controller/config_controller.py @@ -17,7 +17,7 @@ class ConfigController: self.cbpi.register(self) self.path = cbpi.config_folder.get_file_path("config.json") self.path_static = cbpi.config_folder.get_file_path("config.yaml") - self.logger.info("Config folder path : " + os.path.join(Path(self.cbpi.config_folder._rawPath).absolute())) + self.logger.info("Config folder path : " + os.path.join(Path(self.cbpi.config_folder.configFolderPath).absolute())) def get_state(self): diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py index 05024bb..686d5b2 100644 --- a/cbpi/controller/log_file_controller.py +++ b/cbpi/controller/log_file_controller.py @@ -26,7 +26,7 @@ class LogController: self.logger = logging.getLogger(__name__) self.configuration = False self.datalogger = {} - self.logsFolderPath = os.path.join(Path(self.cbpi.config_folder._rawPath).parent.absolute(), 'logs') + self.logsFolderPath = self.cbpi.config_folder.logsFolderPath self.logger.info("Log folder path : " + self.logsFolderPath) def log_data(self, name: str, value: str) -> None: diff --git a/cbpi/controller/system_controller.py b/cbpi/controller/system_controller.py index 24128fe..14d3bf4 100644 --- a/cbpi/controller/system_controller.py +++ b/cbpi/controller/system_controller.py @@ -116,7 +116,7 @@ class SystemController: try: content = backup_file.read() if backup_file and self.allowed_file(filename, 'zip'): - self.path = os.path.join(self.cbpi.config_folder._rawPath, "restored_config.zip") + self.path = os.path.join(self.cbpi.config_folder.configFolderPath, "restored_config.zip") f=open(self.path, "wb") f.write(content) From f924e1a683f2e416539270761ce4384c1a8a2e8c Mon Sep 17 00:00:00 2001 From: prash3r Date: Wed, 14 Sep 2022 13:54:58 +0200 Subject: [PATCH 19/48] adds journalctl arg '--output cat' (log exports), because cbpi already writes timestamps into the logs and we dont need two timestamps when exporting. This commit also reverts to using the same formatter for all logHandlers. --- cbpi/cli.py | 10 ---------- cbpi/controller/system_controller.py | 4 ++-- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/cbpi/cli.py b/cbpi/cli.py index 83a2582..1496764 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -17,7 +17,6 @@ from importlib_metadata import metadata from tabulate import tabulate from PyInquirer import prompt, print_json import platform -import subprocess class CraftBeerPiCli(): def __init__(self, config) -> None: @@ -239,15 +238,6 @@ def main(context, config_folder_path, logs_folder_path, debug_log_level): formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s') logging.basicConfig(format=formatter,level=debug_log_level, stream=logging.StreamHandler()) logger = logging.getLogger() - try: - result = subprocess.run(['journalctl', '--since', '2 hours ago', '-u', 'craftbeerpi.service' ], stdout=subprocess.PIPE) - # journalctl is present, we assume we are running in production. - # We therefore omit the timestamp from the stdout log handler formatter because timestamps are added to the logs by journalctl anyway - logger.handlers[0].setFormatter('%(levelname)s - %(name)s - %(message)s') - except: - # journalctl command seems to not be present. - # We assume we are in the dev container and keep writing timestampts to stdout for vscode terminal output - logger.warning("journalctl command error - assuming dev container execution and writing timestamps to stdout") try: if not os.path.isdir(logs_folder_path): logger.info(f"logs folder '{logs_folder_path}' doesnt exist and we are trying to create it") diff --git a/cbpi/controller/system_controller.py b/cbpi/controller/system_controller.py index 14d3bf4..b683679 100644 --- a/cbpi/controller/system_controller.py +++ b/cbpi/controller/system_controller.py @@ -56,9 +56,9 @@ class SystemController: output_filename="cbpi4_log.zip" if logtime == "b": - os.system('journalctl -b -u craftbeerpi.service > {}'.format(fullname)) + os.system('journalctl -b -u craftbeerpi.service --output cat > {}'.format(fullname)) else: - os.system('journalctl --since \"{} hours ago\" -u craftbeerpi.service > {}'.format(logtime, fullname)) + os.system('journalctl --since \"{} hours ago\" -u craftbeerpi.service --output cat > {}'.format(logtime, fullname)) os.system('cbpi plugins > {}'.format(fullpluginname)) From cfc876f2f4aefcd9e7d9e68eadcaaeca5f14458c Mon Sep 17 00:00:00 2001 From: prash3r Date: Wed, 14 Sep 2022 15:08:59 +0200 Subject: [PATCH 20/48] KBH import with and without whirlpool additions --- cbpi/controller/upload_controller.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index 2379faf..92aec4f 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -202,11 +202,6 @@ class UploadController: for whirl in whirlpool: hops.remove(whirl) - print(whirlpool) - print(hops) - - - # get the misc addition times c.execute('SELECT Zugabedauer FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) miscs = c.fetchall() @@ -318,8 +313,11 @@ class UploadController: await self.create_step(step_string) - await self.create_Whirlpool_Cooldown(str(abs(whirlpool[0][0]))) # from kbh this value comes as negative but must be positive - + if not whirlpool: + await self.create_Whirlpool_Cooldown() + else : + await self.create_Whirlpool_Cooldown(str(abs(whirlpool[0][0]))) # from kbh this value comes as negative but must be positive + self.cbpi.notify('KBH Recipe created', name, NotificationType.INFO) except: From 706bbac9f0a84e06102d31d9c176e24e0c22526d Mon Sep 17 00:00:00 2001 From: prash3r Date: Wed, 14 Sep 2022 19:04:58 +0200 Subject: [PATCH 21/48] keeps default dev config unchanged. this commit moves the default config into a seperate folder. And it adds a preLaunchTask which copies the default dev config to the used dev config. This is done with the cp option '-ru' so if the files in the target dir (current config) are newer they are not overwritten. --- .../actor.json | 0 .../chromium.desktop | 0 .../config.json | 0 .../config.yaml | 0 .../craftbeerpi.service | 0 .../dashboard/cbpi_dashboard_1.json | 0 .../dashboard/widgets/.gitkeep | 0 .../fermenter_data.json | 0 .../fermenterrecipes/.gitkeep | 0 .../kettle.json | 0 .../recipes/.gitkeep | 0 .../sensor.json | 0 .../step_data.json | 0 .../upload/.gitkeep | 0 .vscode/launch.json | 3 ++- .vscode/tasks.json | 20 +++++++++++++++++++ cbpi/configFolder.py | 3 ++- 17 files changed, 24 insertions(+), 2 deletions(-) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/actor.json (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/chromium.desktop (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/config.json (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/config.yaml (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/craftbeerpi.service (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/dashboard/cbpi_dashboard_1.json (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/dashboard/widgets/.gitkeep (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/fermenter_data.json (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/fermenterrecipes/.gitkeep (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/kettle.json (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/recipes/.gitkeep (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/sensor.json (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/step_data.json (100%) rename .devcontainer/{cbpi-dev-config => cbpi-default-dev-config}/upload/.gitkeep (100%) create mode 100644 .vscode/tasks.json diff --git a/.devcontainer/cbpi-dev-config/actor.json b/.devcontainer/cbpi-default-dev-config/actor.json similarity index 100% rename from .devcontainer/cbpi-dev-config/actor.json rename to .devcontainer/cbpi-default-dev-config/actor.json diff --git a/.devcontainer/cbpi-dev-config/chromium.desktop b/.devcontainer/cbpi-default-dev-config/chromium.desktop similarity index 100% rename from .devcontainer/cbpi-dev-config/chromium.desktop rename to .devcontainer/cbpi-default-dev-config/chromium.desktop diff --git a/.devcontainer/cbpi-dev-config/config.json b/.devcontainer/cbpi-default-dev-config/config.json similarity index 100% rename from .devcontainer/cbpi-dev-config/config.json rename to .devcontainer/cbpi-default-dev-config/config.json diff --git a/.devcontainer/cbpi-dev-config/config.yaml b/.devcontainer/cbpi-default-dev-config/config.yaml similarity index 100% rename from .devcontainer/cbpi-dev-config/config.yaml rename to .devcontainer/cbpi-default-dev-config/config.yaml diff --git a/.devcontainer/cbpi-dev-config/craftbeerpi.service b/.devcontainer/cbpi-default-dev-config/craftbeerpi.service similarity index 100% rename from .devcontainer/cbpi-dev-config/craftbeerpi.service rename to .devcontainer/cbpi-default-dev-config/craftbeerpi.service diff --git a/.devcontainer/cbpi-dev-config/dashboard/cbpi_dashboard_1.json b/.devcontainer/cbpi-default-dev-config/dashboard/cbpi_dashboard_1.json similarity index 100% rename from .devcontainer/cbpi-dev-config/dashboard/cbpi_dashboard_1.json rename to .devcontainer/cbpi-default-dev-config/dashboard/cbpi_dashboard_1.json diff --git a/.devcontainer/cbpi-dev-config/dashboard/widgets/.gitkeep b/.devcontainer/cbpi-default-dev-config/dashboard/widgets/.gitkeep similarity index 100% rename from .devcontainer/cbpi-dev-config/dashboard/widgets/.gitkeep rename to .devcontainer/cbpi-default-dev-config/dashboard/widgets/.gitkeep diff --git a/.devcontainer/cbpi-dev-config/fermenter_data.json b/.devcontainer/cbpi-default-dev-config/fermenter_data.json similarity index 100% rename from .devcontainer/cbpi-dev-config/fermenter_data.json rename to .devcontainer/cbpi-default-dev-config/fermenter_data.json diff --git a/.devcontainer/cbpi-dev-config/fermenterrecipes/.gitkeep b/.devcontainer/cbpi-default-dev-config/fermenterrecipes/.gitkeep similarity index 100% rename from .devcontainer/cbpi-dev-config/fermenterrecipes/.gitkeep rename to .devcontainer/cbpi-default-dev-config/fermenterrecipes/.gitkeep diff --git a/.devcontainer/cbpi-dev-config/kettle.json b/.devcontainer/cbpi-default-dev-config/kettle.json similarity index 100% rename from .devcontainer/cbpi-dev-config/kettle.json rename to .devcontainer/cbpi-default-dev-config/kettle.json diff --git a/.devcontainer/cbpi-dev-config/recipes/.gitkeep b/.devcontainer/cbpi-default-dev-config/recipes/.gitkeep similarity index 100% rename from .devcontainer/cbpi-dev-config/recipes/.gitkeep rename to .devcontainer/cbpi-default-dev-config/recipes/.gitkeep diff --git a/.devcontainer/cbpi-dev-config/sensor.json b/.devcontainer/cbpi-default-dev-config/sensor.json similarity index 100% rename from .devcontainer/cbpi-dev-config/sensor.json rename to .devcontainer/cbpi-default-dev-config/sensor.json diff --git a/.devcontainer/cbpi-dev-config/step_data.json b/.devcontainer/cbpi-default-dev-config/step_data.json similarity index 100% rename from .devcontainer/cbpi-dev-config/step_data.json rename to .devcontainer/cbpi-default-dev-config/step_data.json diff --git a/.devcontainer/cbpi-dev-config/upload/.gitkeep b/.devcontainer/cbpi-default-dev-config/upload/.gitkeep similarity index 100% rename from .devcontainer/cbpi-dev-config/upload/.gitkeep rename to .devcontainer/cbpi-default-dev-config/upload/.gitkeep diff --git a/.vscode/launch.json b/.vscode/launch.json index 73c363b..a3c07c1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,8 @@ "type": "python", "request": "launch", "module": "run", - "args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "start"] + "args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "start"], + "preLaunchTask": "copy default cbpi config files if dev config files dont exist" }, { diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..05db7ee --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "copy default cbpi config files if dev config files dont exist", + "type": "shell", + "command": "cp -ru ${workspaceFolder}/.devcontainer/cbpi-default-dev-config/. ${workspaceFolder}/.devcontainer/cbpi-dev-config", + "windows": { + "command": "echo 'this pre debug task should only be run inside the docker dev container - doing nothing instead'" + }, + "group": "build", + "presentation": { + "reveal": "silent", + "panel": "shared" + } + } + ] +} \ No newline at end of file diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index d3690cb..fc21aa5 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -14,7 +14,8 @@ class ConfigFolder: def __init__(self, configFolderPath, logsFolderPath): self.configFolderPath = configFolderPath self.logsFolderPath = logsFolderPath - self.logger.info("Config folder path : " + os.path.join(Path(self.cbpi.config_folder.configFolderPath).absolute())) + print("config folder path : " + configFolderPath) + print("logs folder path : " + logsFolderPath) def config_file_exists(self, path): return os.path.exists(self.get_file_path(path)) From ac9b599619c348ebae5c4fa96f4d3df5d3d5f70e Mon Sep 17 00:00:00 2001 From: prash3r Date: Wed, 14 Sep 2022 19:36:51 +0200 Subject: [PATCH 22/48] check_for_setup() now checks for provided logs folder path --- cbpi/configFolder.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index fc21aa5..88ee8b8 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -97,7 +97,6 @@ class ConfigFolder: ['dashboard/widgets', 'folder'], ['dashboard', 'folder'], ['fermenterrecipes', 'folder'], - ['../logs', 'folder'], ['recipes', 'folder'], ['upload', 'folder'] ] @@ -174,7 +173,6 @@ class ConfigFolder: def create_folders(self): pathlib.Path(self.configFolderPath).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self.configFolderPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True) - #pathlib.Path(os.path.join(self.configFolderPath, '..','logs')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self.configFolderPath, 'recipes')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self.configFolderPath, 'fermenterrecipes')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self.configFolderPath, 'upload')).mkdir(parents=True, exist_ok=True) From 1fa3f8899725223250e36bd4fe92fa0661aa5491 Mon Sep 17 00:00:00 2001 From: prash3r Date: Fri, 16 Sep 2022 10:25:31 +0200 Subject: [PATCH 23/48] adds the 'cbpi create' command to vscode. also initilizes plugin names with dashes instead of underscores. everything now runs as root inside the dev container (otherwise the permissions for cbpi create wouldnt be sufficient). --- .devcontainer/devcontainer.json | 2 +- .gitignore | 4 +++- .vscode/launch.json | 8 ++++++++ cbpi/cli.py | 2 +- cbpi/configFolder.py | 2 ++ 5 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 08f216c..ae59030 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -47,5 +47,5 @@ //"postCreateCommand": "pip3 install -r ./requirements.txt", // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "vscode" + // "remoteUser": "vscode" } diff --git a/.gitignore b/.gitignore index 20ad518..32b01ba 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,6 @@ node_modules config/* logs/ .coverage -.devcontainer/cbpi-dev-config/* \ No newline at end of file +.devcontainer/cbpi-dev-config/* +cbpi4-* +temp* \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index a3c07c1..1dd8992 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -14,6 +14,14 @@ "preLaunchTask": "copy default cbpi config files if dev config files dont exist" }, + { + "name": "create CraftBeerPi4 plugin", + "type": "python", + "request": "launch", + "module": "run", + "args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "create"] + }, + { "name": "setup CraftBeerPi4: create config folder structure", "type": "python", diff --git a/cbpi/cli.py b/cbpi/cli.py index 1496764..fcec9de 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -87,7 +87,7 @@ class CraftBeerPiCli(): answers = prompt(questions) - name = "cbpi4_" + answers["name"] + name = "cbpi4-" + str(answers["name"]).replace('_', '-').replace(' ', '-') if os.path.exists(os.path.join(".", name)) is True: print("Cant create Plugin. Folder {} already exists ".format(name)) return diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index 88ee8b8..954f1a3 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -97,6 +97,7 @@ class ConfigFolder: ['dashboard/widgets', 'folder'], ['dashboard', 'folder'], ['fermenterrecipes', 'folder'], + [self.logsFolderPath, 'folder'], ['recipes', 'folder'], ['upload', 'folder'] ] @@ -172,6 +173,7 @@ class ConfigFolder: def create_folders(self): pathlib.Path(self.configFolderPath).mkdir(parents=True, exist_ok=True) + pathlib.Path(self.logsFolderPath).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self.configFolderPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self.configFolderPath, 'recipes')).mkdir(parents=True, exist_ok=True) pathlib.Path(os.path.join(self.configFolderPath, 'fermenterrecipes')).mkdir(parents=True, exist_ok=True) From 6bf1b14a36d1c3a4d3dfe229dca87ff84d993450 Mon Sep 17 00:00:00 2001 From: prash3r Date: Fri, 16 Sep 2022 16:45:33 +0200 Subject: [PATCH 24/48] notify clients on log events worse than INFO --- cbpi/controller/notification_controller.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cbpi/controller/notification_controller.py b/cbpi/controller/notification_controller.py index 5d2b4ef..26c25b4 100644 --- a/cbpi/controller/notification_controller.py +++ b/cbpi/controller/notification_controller.py @@ -1,4 +1,5 @@ import asyncio +from email import message from cbpi.api.dataclasses import NotificationType import logging import shortuuid @@ -10,9 +11,23 @@ class NotificationController: ''' self.cbpi = cbpi self.logger = logging.getLogger(__name__) + logging.root.addFilter(self.notify_log_event) self.callback_cache = {} self.listener = {} + def notify_log_event(self, record): + try: + if record.levelno > 20: + # on log events higher then INFO we want to notify all clients + type = NotificationType.WARNING + if record.levelno > 30: + type = NotificationType.ERROR + self.cbpi.notify(title=f"{record.levelname}", message=record.msg, type = type) + except Exception as e: + pass + finally: + return True + def add_listener(self, method): listener_id = shortuuid.uuid() self.listener[listener_id] = method From fea1ba04f0c2f6d4b7db878a0d58b36b56ce904d Mon Sep 17 00:00:00 2001 From: prash3r Date: Fri, 16 Sep 2022 18:56:52 +0200 Subject: [PATCH 25/48] corrected some more places where ./logs was used instead of the dynamically given logs path. --- cbpi/controller/log_file_controller.py | 7 +++---- cbpi/http_endpoints/http_log.py | 3 ++- cbpi/http_endpoints/http_system.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py index 686d5b2..05e9d32 100644 --- a/cbpi/controller/log_file_controller.py +++ b/cbpi/controller/log_file_controller.py @@ -160,7 +160,7 @@ class LogController: for id in ids: # df = pd.read_csv("./logs/sensor_%s.log" % id, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime',"Values"], header=None) # concat all logs - all_filenames = glob.glob(os.path.join(self.logsFolderPath,f"./logs/sensor_{id}.log*")) + all_filenames = glob.glob(os.path.join(self.logsFolderPath,f"sensor_{id}.log*")) df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Values'], header=None) for f in all_filenames]) df = df.resample('60s').max() df = df.dropna() @@ -179,8 +179,7 @@ class LogController: return [os.path.basename(x) for x in os.path.join(self.logsFolderPath, f"sensor_{name}.log*")] def clear_log(self, name:str ) -> str: - - all_filenames = os.path.join(self.logsFolderPath, f"sensor_{name}.log*") + all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*")) for f in all_filenames: os.remove(f) @@ -216,7 +215,7 @@ class LogController: """ formatted_time = strftime("%Y-%m-%d-%H_%M_%S", localtime()) - file_name = os.path.join(self.logsFolderPath, f"./logs/{formatted_time}-sensor-{name}.zip" % (formatted_time, name)) + file_name = os.path.join(self.logsFolderPath, f"{formatted_time}-sensor-{name}.zip" % (formatted_time, name)) zip = zipfile.ZipFile(file_name, 'w', zipfile.ZIP_DEFLATED) all_filenames = os.path.join(self.logsFolderPath, f"sensor_{name}.log*") for f in all_filenames: diff --git a/cbpi/http_endpoints/http_log.py b/cbpi/http_endpoints/http_log.py index e898460..f452026 100644 --- a/cbpi/http_endpoints/http_log.py +++ b/cbpi/http_endpoints/http_log.py @@ -2,6 +2,7 @@ from cbpi.utils.encoder import ComplexEncoder from aiohttp import web from cbpi.utils.utils import json_dumps from cbpi.api import request_mapping +import os import json class LogHttpEndpoints: @@ -83,7 +84,7 @@ class LogHttpEndpoints: ) await response.prepare(request) log_name = request.match_info['name'] - with open('./logs/%s.zip' % log_name, 'rb') as file: + with open(os.path.join(self.cbpi.logsFolderPath, '%s.zip' % log_name), 'rb') as file: for line in file.readlines(): await response.write(line) diff --git a/cbpi/http_endpoints/http_system.py b/cbpi/http_endpoints/http_system.py index c21c4cf..823ef6f 100644 --- a/cbpi/http_endpoints/http_system.py +++ b/cbpi/http_endpoints/http_system.py @@ -44,7 +44,7 @@ class SystemHttpEndpoints: async def http_get_log(self, request): result = [] file_pattern = re.compile("^(\w+.).log(.?\d*)") - for filename in sorted(os.listdir("./logs"), reverse=True): # + for filename in sorted(os.listdir(self.cbpi.logsFolderPath), reverse=True): if file_pattern.match(filename): result.append(filename) return web.json_response(result) From e0d809c3a31b0b37de525af12ca0c135f425e4ff Mon Sep 17 00:00:00 2001 From: prash3r Date: Sat, 24 Sep 2022 12:58:26 +0200 Subject: [PATCH 26/48] additional requirements.txt for devcontainer only inside the dev container default config there now is a 'additional-dev-requirements.txt' file that allows us to ship additional python packages requirements with a default development config. --- .devcontainer/Dockerfile | 7 ++++--- .../additional-dev-requirements.txt | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 .devcontainer/cbpi-default-dev-config/additional-dev-requirements.txt diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 732ba18..41c4890 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,9 +9,10 @@ RUN apt-get install --no-install-recommends -y \ && rm -rf /var/lib/apt/lists/* RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel -# Install craftbeerpi requirements for better caching -COPY ./requirements.txt /workspace/requirements.txt -RUN pip3 install --no-cache-dir -r /workspace/requirements.txt +# Install craftbeerpi requirements and additional-dev-requirements for better caching +COPY ./requirements.txt ./.devcontainer/cbpi-default-dev-config/additional-dev-requirements.txt /workspace/ +RUN cat /workspace/additional-dev-requirements.txt 2>/dev/null 1>> /workspace/requirements.txt \ + && pip3 install --no-cache-dir -r /workspace/requirements.txt # Install current version of cbpi-ui RUN mkdir /opt/downloads \ diff --git a/.devcontainer/cbpi-default-dev-config/additional-dev-requirements.txt b/.devcontainer/cbpi-default-dev-config/additional-dev-requirements.txt new file mode 100644 index 0000000..a1b4c97 --- /dev/null +++ b/.devcontainer/cbpi-default-dev-config/additional-dev-requirements.txt @@ -0,0 +1 @@ +cbpi4-SimulatedSensor==0.0.2 \ No newline at end of file From e7aa0a64c7dd9a7b597d0634305efeb5b5c225a4 Mon Sep 17 00:00:00 2001 From: prash3r Date: Sat, 24 Sep 2022 15:19:34 +0200 Subject: [PATCH 27/48] repairs log_file_controller.py. I seem to have exidentally search and replaced some needed glob.glob calls when adding dinamic log location, which should now be repaired. There also was a mysterious cbpi_dashboard_1.json appearing in the wrong folder which now inside the dashboard folder. I also figured out how to run tests locally, they should now respect the dynamic folder paths. --- .vscode/launch.json | 8 ++++++++ cbpi/configFolder.py | 3 +++ cbpi/controller/dashboard_controller.py | 10 +++++----- cbpi/controller/log_file_controller.py | 8 ++++---- tests/cbpi_config_fixture.py | 3 ++- tests/test_cli.py | 2 +- tests/test_logger.py | 4 ++-- 7 files changed, 25 insertions(+), 13 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1dd8992..e24e1e9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -28,6 +28,14 @@ "request": "launch", "module": "run", "args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "setup"] + }, + + { + "name": "run tests", + "type": "python", + "request": "launch", + "module": "pytest", + "args": ["tests"] } ] } \ No newline at end of file diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index 954f1a3..ef2169b 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -22,6 +22,9 @@ class ConfigFolder: def get_file_path(self, file): return os.path.join(self.configFolderPath, file) + + def get_dashboard_path(self, file): + return os.path.join(self.configFolderPath, "dashboard", file) def get_upload_file(self, file): return os.path.join(self.configFolderPath, 'upload', file) diff --git a/cbpi/controller/dashboard_controller.py b/cbpi/controller/dashboard_controller.py index 2f39af5..feef806 100644 --- a/cbpi/controller/dashboard_controller.py +++ b/cbpi/controller/dashboard_controller.py @@ -18,14 +18,14 @@ class DashboardController: self.logger = logging.getLogger(__name__) self.cbpi.register(self) - self.path = cbpi.config_folder.get_file_path("cbpi_dashboard_1.json") + self.path = cbpi.config_folder.get_dashboard_path("cbpi_dashboard_1.json") async def init(self): pass async def get_content(self, dashboard_id): try: - self.path = self.cbpi.config_folder.get_file_path("cbpi_dashboard_"+ str(dashboard_id) +".json") + self.path = self.cbpi.config_folder.get_dashboard_path("cbpi_dashboard_"+ str(dashboard_id) +".json") logging.info(self.path) with open(self.path) as json_file: data = json.load(json_file) @@ -35,21 +35,21 @@ class DashboardController: async def add_content(self, dashboard_id, data): print(data) - self.path = self.cbpi.config_folder.get_file_path("cbpi_dashboard_" + str(dashboard_id)+ ".json") + self.path = self.cbpi.config_folder.get_dashboard_path("cbpi_dashboard_" + str(dashboard_id)+ ".json") with open(self.path, 'w') as outfile: json.dump(data, outfile, indent=4, sort_keys=True) self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Saved Successfully", type=NotificationType.SUCCESS) return {"status": "OK"} async def delete_content(self, dashboard_id): - self.path = self.cbpi.config_folder.get_file_path("cbpi_dashboard_"+ str(dashboard_id)+ ".json") + self.path = self.cbpi.config_folder.get_dashboard_path("cbpi_dashboard_"+ str(dashboard_id)+ ".json") if os.path.exists(self.path): os.remove(self.path) self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Deleted Successfully", type=NotificationType.SUCCESS) async def get_custom_widgets(self): - path = os.path.join(self.cbpi.config_folder.get_file_path("dashboard"), "widgets") + path = self.cbpi.config_folder.get_dashboard_path("widgets") onlyfiles = [os.path.splitext(f)[0] for f in sorted(listdir(path)) if isfile(join(path, f)) and f.endswith(".svg")] return onlyfiles diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py index 05e9d32..50790e2 100644 --- a/cbpi/controller/log_file_controller.py +++ b/cbpi/controller/log_file_controller.py @@ -118,7 +118,7 @@ class LogController: for name in names: # get all log names - all_filenames = os.path.join(self.logsFolderPath, f"sensor_{name}.log*") + all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*")) # concat all logs df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', name], header=None) for f in all_filenames]) logging.info("Read all files for {}".format(names)) @@ -176,7 +176,7 @@ class LogController: :return: list of log file names ''' - return [os.path.basename(x) for x in os.path.join(self.logsFolderPath, f"sensor_{name}.log*")] + return [os.path.basename(x) for x in glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*"))] def clear_log(self, name:str ) -> str: all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*")) @@ -215,9 +215,9 @@ class LogController: """ formatted_time = strftime("%Y-%m-%d-%H_%M_%S", localtime()) - file_name = os.path.join(self.logsFolderPath, f"{formatted_time}-sensor-{name}.zip" % (formatted_time, name)) + file_name = os.path.join(self.logsFolderPath, f"{formatted_time}-sensor-{name}.zip") zip = zipfile.ZipFile(file_name, 'w', zipfile.ZIP_DEFLATED) - all_filenames = os.path.join(self.logsFolderPath, f"sensor_{name}.log*") + all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*")) for f in all_filenames: zip.write(os.path.join(f)) zip.close() diff --git a/tests/cbpi_config_fixture.py b/tests/cbpi_config_fixture.py index b9824ba..2510c21 100644 --- a/tests/cbpi_config_fixture.py +++ b/tests/cbpi_config_fixture.py @@ -19,5 +19,6 @@ class CraftBeerPiTestCase(AioHTTPTestCase): def configuration(self): test_directory = os.path.dirname(__file__) test_config_directory = os.path.join(test_directory, 'cbpi-test-config') - configFolder = ConfigFolder(test_config_directory) + test_logs_directory = os.path.join(test_directory, 'logs') + configFolder = ConfigFolder(test_config_directory, test_logs_directory) return configFolder diff --git a/tests/test_cli.py b/tests/test_cli.py index 0a9f705..5de4f03 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -9,7 +9,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %( class CLITest(unittest.TestCase): def test_list(self): - cli = CraftBeerPiCli(ConfigFolder("./cbpi-test-config")) + cli = CraftBeerPiCli(ConfigFolder("./cbpi-test-config", './logs')) # inside tests folder cli.plugins_list() if __name__ == '__main__': diff --git a/tests/test_logger.py b/tests/test_logger.py index d626ab0..2f84642 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -10,11 +10,11 @@ class LoggerTestCase(CraftBeerPiTestCase): @unittest_run_loop async def test_log_data(self): - os.makedirs("./logs", exist_ok=True) + os.makedirs(os.path.join(".", "tests", "logs"), exist_ok=True) log_name = "test" #clear all logs self.cbpi.log.clear_log(log_name) - assert len(glob.glob('./logs/sensor_%s.log*' % log_name)) == 0 + assert len(glob.glob(os.path.join(".", "tests", "logs", f"sensor_{log_name}.log*"))) == 0 # write log entries for i in range(5): From 50fa87d6dff8d7a17d9d48740e3ee4ee4af59c3c Mon Sep 17 00:00:00 2001 From: prash3r Date: Sat, 24 Sep 2022 21:55:36 +0200 Subject: [PATCH 28/48] repairs plugin creation inside the devcontainer strange windows volume behaviour needs a small delay between template folder unzip and rename. reverts user changes of the dev container which werent the cause of this problem. --- .devcontainer/devcontainer.json | 2 +- cbpi/cli.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ae59030..08f216c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -47,5 +47,5 @@ //"postCreateCommand": "pip3 install -r ./requirements.txt", // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. - // "remoteUser": "vscode" + "remoteUser": "vscode" } diff --git a/cbpi/cli.py b/cbpi/cli.py index fcec9de..bc3b13d 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -17,6 +17,7 @@ from importlib_metadata import metadata from tabulate import tabulate from PyInquirer import prompt, print_json import platform +import time class CraftBeerPiCli(): def __init__(self, config) -> None: @@ -99,8 +100,10 @@ class CraftBeerPiCli(): with ZipFile('temp.zip', 'r') as repo_zip: repo_zip.extractall() + + time.sleep(1) # windows dev container permissions problem otherwise - os.rename("./craftbeerpi4-plugin-template-main", os.path.join(".", name)) + os.rename(os.path.join(".","craftbeerpi4-plugin-template-main"), os.path.join(".", name)) os.rename(os.path.join(".", name, "src"), os.path.join(".", name, name)) import jinja2 From 2bc5bbb183ce265e9240a5bf269d000e979cc2bb Mon Sep 17 00:00:00 2001 From: lopelex Date: Wed, 28 Sep 2022 16:56:14 +0200 Subject: [PATCH 29/48] add hop text --- cbpi/controller/upload_controller.py | 103 ++++++++++++++++----------- cbpi/extension/mashstep/__init__.py | 17 +++-- 2 files changed, 76 insertions(+), 44 deletions(-) diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index f0a32c2..7302f18 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -193,15 +193,15 @@ class UploadController: pass # get the hop addition times - c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze <> 1 AND Vorderwuerze <> 5 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zeit, Name FROM Hopfengaben WHERE Vorderwuerze <> 1 AND Vorderwuerze <> 5 AND SudID = ?', (Recipe_ID,)) hops = c.fetchall() # get the misc addition times - c.execute('SELECT Zugabedauer FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zugabedauer, Name FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) miscs = c.fetchall() try: - c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze = 1 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zeit, Name FROM Hopfengaben WHERE Vorderwuerze = 1 AND SudID = ?', (Recipe_ID,)) FW_Hops = c.fetchall() FirstWort = self.getFirstWort(FW_Hops,"kbh") except: @@ -293,12 +293,18 @@ class UploadController: "Timer": BoilTime, "First_Wort": FirstWort, "LidAlert": "Yes", - "Hop_1": Hops[0], - "Hop_2": Hops[1], - "Hop_3": Hops[2], - "Hop_4": Hops[3], - "Hop_5": Hops[4], - "Hop_6": Hops[5] + "Hop_1": Hops[0][0], + "Hop_1_text": Hops[0][1], + "Hop_2": Hops[1][0], + "Hop_2_text": Hops[1][1], + "Hop_3": Hops[2][0], + "Hop_3_text": Hops[2][1], + "Hop_4": Hops[3][0], + "Hop_4_text": Hops[3][1], + "Hop_5": Hops[4][0], + "Hop_5_text": Hops[4][1], + "Hop_6": Hops[5][0], + "Hop_6_text": Hops[5][1] }, "status_text": "", "status": "I", @@ -512,12 +518,18 @@ class UploadController: "Timer": step_time, "First_Wort": FirstWort, "LidAlert": LidAlert, - "Hop_1": Hops[0], - "Hop_2": Hops[1], - "Hop_3": Hops[2], - "Hop_4": Hops[3], - "Hop_5": Hops[4], - "Hop_6": Hops[5] + "Hop_1": Hops[0][0], + "Hop_1_text": Hops[0][1], + "Hop_2": Hops[1][0], + "Hop_2_text": Hops[1][1], + "Hop_3": Hops[2][0], + "Hop_3_text": Hops[2][1], + "Hop_4": Hops[3][0], + "Hop_4_text": Hops[3][1], + "Hop_5": Hops[4][0], + "Hop_5_text": Hops[4][1], + "Hop_6": Hops[5][0], + "Hop_6_text": Hops[5][1] }, "status_text": "", "status": "I", @@ -658,12 +670,18 @@ class UploadController: "Timer": step_time, "First_Wort": FirstWort, "LidAlert": LidAlert, - "Hop_1": Hops[0], - "Hop_2": Hops[1], - "Hop_3": Hops[2], - "Hop_4": Hops[3], - "Hop_5": Hops[4], - "Hop_6": Hops[5] + "Hop_1": Hops[0][0], + "Hop_1_text": Hops[0][1], + "Hop_2": Hops[1][0], + "Hop_2_text": Hops[1][1], + "Hop_3": Hops[2][0], + "Hop_3_text": Hops[2][1], + "Hop_4": Hops[3][0], + "Hop_4_text": Hops[3][1], + "Hop_5": Hops[4][0], + "Hop_5_text": Hops[4][1], + "Hop_6": Hops[5][0], + "Hop_6_text": Hops[5][1] }, "status_text": "", "status": "I", @@ -860,12 +878,18 @@ class UploadController: "Timer": step_time, "First_Wort": FirstWort, "LidAlert": LidAlert, - "Hop_1": Hops[0], - "Hop_2": Hops[1], - "Hop_3": Hops[2], - "Hop_4": Hops[3], - "Hop_5": Hops[4], - "Hop_6": Hops[5] + "Hop_1": Hops[0][0], + "Hop_1_text": Hops[0][1], + "Hop_2": Hops[1][0], + "Hop_2_text": Hops[1][1], + "Hop_3": Hops[2][0], + "Hop_3_text": Hops[2][1], + "Hop_4": Hops[3][0], + "Hop_4_text": Hops[3][1], + "Hop_5": Hops[4][0], + "Hop_5_text": Hops[4][1], + "Hop_6": Hops[5][0], + "Hop_6_text": Hops[5][1] }, "status_text": "", "status": "I", @@ -888,41 +912,40 @@ class UploadController: ## Hops which are not used in the boil step should not cause alerts if use != 'Aroma' and use != 'Boil': continue - alerts.append(float(hop.find('TIME').text)) + alerts.append([float(hop.find('TIME').text), hop.find('NAME').text]) elif recipe_type == "bf": use = hop['use'] if use != 'Aroma' and use != 'Boil': continue - alerts.append(float(hop['time'])) + alerts.append([float(hop['time']), hop['name']]) ## TODO: Testing elif recipe_type == "kbh": - alerts.append(float(hop[0])) + alerts.append([float(hop[0]), hop[1]]) elif recipe_type == "json": - alerts.append(float(hop['time'])) + alerts.append([float(hop['time']), hop['name']]) ## There might also be miscelaneous additions during boild time if miscs is not None: for misc in miscs: if recipe_type == "xml": - alerts.append(float(misc.find('TIME').text)) + alerts.append([float(misc.find('TIME').text), misc.find('NAME').text]) elif recipe_type == "bf": use = misc['use'] if use != 'Aroma' and use != 'Boil': continue - alerts.append(float(misc['time'])) + alerts.append([float(misc['time']), misc['name']]) ## TODO: Testing elif recipe_type == "kbh": - alerts.append(float(misc[0])) + alerts.append([float(misc[0]), misc[1]]) elif recipe_type == "json": - alerts.append(float(misc['time'])) - ## Dedupe and order the additions by their time, to prevent multiple alerts at the same time - alerts = sorted(list(set(alerts))) + alerts.append([float(misc['time']), misc['name']]) + ## Dedupe and order the additions by their time, to prevent? multiple alerts at the same time ## CBP should have these additions in reverse - alerts.reverse() + alerts = sorted(alerts, key=lambda x:x[0], reverse=True) hop_alerts = [] for i in range(0,6): try: - hop_alerts.append(str(int(alerts[i]))) + hop_alerts.append(alerts[i]) except: - hop_alerts.append(None) + hop_alerts.append([None, None]) return hop_alerts def getFirstWort(self, hops, recipe_type): diff --git a/cbpi/extension/mashstep/__init__.py b/cbpi/extension/mashstep/__init__.py index c830de2..1ed3c43 100644 --- a/cbpi/extension/mashstep/__init__.py +++ b/cbpi/extension/mashstep/__init__.py @@ -323,11 +323,17 @@ class ActorStep(CBPiStep): Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Kettlelogic automatically on and off -> Yes"), Property.Select("First_Wort", options=["Yes","No"], description="First Wort Hop alert if set to Yes"), Property.Number("Hop_1", configurable = True, description="First Hop alert (minutes before finish)"), + Property.Text("Hop_1_text", configurable = True, description="First Hop alert text"), Property.Number("Hop_2", configurable=True, description="Second Hop alert (minutes before finish)"), + Property.Text("Hop_2_text", configurable = True, description="Second Hop alert text"), Property.Number("Hop_3", configurable=True, description="Third Hop alert (minutes before finish)"), + Property.Text("Hop_3_text", configurable = True, description="Third Hop alert text"), Property.Number("Hop_4", configurable=True, description="Fourth Hop alert (minutes before finish)"), + Property.Text("Hop_4_text", configurable = True, description="Fourth Hop alert text"), Property.Number("Hop_5", configurable=True, description="Fifth Hop alert (minutes before finish)"), - Property.Number("Hop_6", configurable=True, description="Sixth Hop alert (minutes before finish)")]) + Property.Text("Hop_5_text", configurable = True, description="Fifth Hop alert text"), + Property.Number("Hop_6", configurable=True, description="Sixth Hop alert (minutes before finish)"), + Property.Text("Hop_6_text", configurable = True, description="Sixth Hop alert text")]) class BoilStep(CBPiStep): @action("Start Timer", []) @@ -389,11 +395,14 @@ class BoilStep(CBPiStep): await self.setAutoMode(True) await self.push_update() - async def check_hop_timer(self, number, value): + async def check_hop_timer(self, number, value, text): if value is not None and self.hops_added[number-1] is not True: if self.remaining_seconds != None and self.remaining_seconds <= (int(value) * 60 + 1): self.hops_added[number-1]= True - self.cbpi.notify('Hop Alert', "Please add Hop %s" % number, NotificationType.INFO) + if text is not None and text != "": + self.cbpi.notify('Hop Alert', "Please add %s (%s)" % (text, number), NotificationType.INFO) + else: + self.cbpi.notify('Hop Alert', "Please add Hop %s" % number, NotificationType.INFO) async def on_stop(self): await self.timer.stop() @@ -426,7 +435,7 @@ class BoilStep(CBPiStep): self.cbpi.notify(self.name, 'Timer started. Estimated completion: {}'.format(estimated_completion_time.strftime("%H:%M")), NotificationType.INFO) else: for x in range(1, 6): - await self.check_hop_timer(x, self.props.get("Hop_%s" % x, None)) + await self.check_hop_timer(x, self.props.get("Hop_%s" % x, None), self.props.get("Hop_%s_text" % x, None)) return StepResult.DONE From d1c38af3ae382367f4acfa128fb4cc3f00af07ba Mon Sep 17 00:00:00 2001 From: lopelex Date: Thu, 29 Sep 2022 12:11:06 +0200 Subject: [PATCH 30/48] add First_Wort_text and fix error in json_recipe_creation #367 --- cbpi/controller/upload_controller.py | 30 +++++++++++++++++++--------- cbpi/extension/mashstep/__init__.py | 8 +++++++- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index 7302f18..240edef 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -291,7 +291,8 @@ class UploadController: "Sensor": self.boilkettle.sensor, "Temp": int(self.BoilTemp), "Timer": BoilTime, - "First_Wort": FirstWort, + "First_Wort": FirstWort[0], + "First_Wort_text": FirstWort[1], "LidAlert": "Yes", "Hop_1": Hops[0][0], "Hop_1_text": Hops[0][1], @@ -363,7 +364,7 @@ class UploadController: elif e["Hopfen_{}_Kochzeit".format(idx)] == "Whirlpool": alert = float(1) else: - self.api.notify(headline="No Number at Hoptime", message="Please change json-File at Hopfen_{}_Kochzeit".format(idx), type="danger") + self.cbpi.notify("No Number at Hoptime", "Please change json-File at Hopfen_{}_Kochzeit".format(idx), NotificationType.ERROR) alert = float(1) hops.append({"name":hops_name,"time":alert}) @@ -516,7 +517,8 @@ class UploadController: "Sensor": sensor, "Temp": step_temp, "Timer": step_time, - "First_Wort": FirstWort, + "First_Wort": FirstWort[0], + "First_Wort_text": FirstWort[1], "LidAlert": LidAlert, "Hop_1": Hops[0][0], "Hop_1_text": Hops[0][1], @@ -668,7 +670,8 @@ class UploadController: "Sensor": sensor, "Temp": step_temp, "Timer": step_time, - "First_Wort": FirstWort, + "First_Wort": FirstWort[0], + "First_Wort_text": FirstWort[1], "LidAlert": LidAlert, "Hop_1": Hops[0][0], "Hop_1_text": Hops[0][1], @@ -876,7 +879,8 @@ class UploadController: "Sensor": sensor, "Temp": step_temp, "Timer": step_time, - "First_Wort": FirstWort, + "First_Wort": FirstWort[0], + "First_Wort_text": FirstWort[1], "LidAlert": LidAlert, "Hop_1": Hops[0][0], "Hop_1_text": Hops[0][1], @@ -937,7 +941,7 @@ class UploadController: alerts.append([float(misc[0]), misc[1]]) elif recipe_type == "json": alerts.append([float(misc['time']), misc['name']]) - ## Dedupe and order the additions by their time, to prevent? multiple alerts at the same time + ## Dedupe and order the additions by their time ## CBP should have these additions in reverse alerts = sorted(alerts, key=lambda x:x[0], reverse=True) hop_alerts = [] @@ -950,9 +954,12 @@ class UploadController: def getFirstWort(self, hops, recipe_type): alert = "No" + names = [] if recipe_type == "kbh": if len(hops) != 0: alert = "Yes" + for hop in hops: + names.append(hop[1]) elif recipe_type == "xml": for hop in hops: use = hop.find('USE').text @@ -960,14 +967,19 @@ class UploadController: if use != 'First Wort': continue alert = "Yes" + names.append(hop.find('NAME').text) elif recipe_type == "bf": for hop in hops: if hop['use'] == "First Wort": alert="Yes" + names.append(hop['name']) ## TODO: Testing elif recipe_type == "json": - for hop in hops: - alert="Yes" - return alert + if len(hops) != 0: + alert = "Yes" + for hop in hops: + names.append(hop['name']) + + return [alert, " and ".join(names)] async def create_Whirlpool_Cooldown(self): # Add Waitstep as Whirlpool diff --git a/cbpi/extension/mashstep/__init__.py b/cbpi/extension/mashstep/__init__.py index 1ed3c43..2c62ce3 100644 --- a/cbpi/extension/mashstep/__init__.py +++ b/cbpi/extension/mashstep/__init__.py @@ -1,4 +1,5 @@ import asyncio +from pickle import NONE from cbpi.api import parameters, Property, action from cbpi.api.step import StepResult, CBPiStep @@ -322,6 +323,7 @@ class ActorStep(CBPiStep): Property.Select(label="LidAlert",options=["Yes","No"], description="Trigger Alert to remove lid if temp is close to boil"), Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Kettlelogic automatically on and off -> Yes"), Property.Select("First_Wort", options=["Yes","No"], description="First Wort Hop alert if set to Yes"), + Property.Text("First_Wort_text", configurable = True, description="First Wort Hop alert text"), Property.Number("Hop_1", configurable = True, description="First Hop alert (minutes before finish)"), Property.Text("Hop_1_text", configurable = True, description="First Hop alert text"), Property.Number("Hop_2", configurable=True, description="Second Hop alert (minutes before finish)"), @@ -373,6 +375,7 @@ class BoilStep(CBPiStep): self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False self.first_wort_hop_flag = False self.first_wort_hop=self.props.get("First_Wort", "No") + self.first_wort_hop_text=self.props.get("First_Wort_text", None) self.hops_added=["","","","","",""] self.remaining_seconds = None @@ -418,7 +421,10 @@ class BoilStep(CBPiStep): async def run(self): if self.first_wort_hop_flag == False and self.first_wort_hop == "Yes": self.first_wort_hop_flag = True - self.cbpi.notify('First Wort Hop Addition!', 'Please add hops for first wort', NotificationType.INFO) + if self.first_wort_hop_text is not None and self.first_wort_hop_text != "": + self.cbpi.notify('First Wort Hop Addition!', 'Please add %s for first wort' % self.first_wort_hop_text, NotificationType.INFO) + else: + self.cbpi.notify('First Wort Hop Addition!', 'Please add hops for first wort', NotificationType.INFO) while self.running == True: await asyncio.sleep(1) From 89da0bec0416a2fc96468ef7b1012ef4b22c2d79 Mon Sep 17 00:00:00 2001 From: lopelex Date: Fri, 30 Sep 2022 07:36:21 +0200 Subject: [PATCH 31/48] remove unused import --- cbpi/extension/mashstep/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cbpi/extension/mashstep/__init__.py b/cbpi/extension/mashstep/__init__.py index 2c62ce3..2feeabc 100644 --- a/cbpi/extension/mashstep/__init__.py +++ b/cbpi/extension/mashstep/__init__.py @@ -1,5 +1,4 @@ import asyncio -from pickle import NONE from cbpi.api import parameters, Property, action from cbpi.api.step import StepResult, CBPiStep From 2a0daf354c7b7519cf19b6b3184cc83758e60dd4 Mon Sep 17 00:00:00 2001 From: prash3r Date: Sun, 2 Oct 2022 18:32:40 +0200 Subject: [PATCH 32/48] moves cbpi_dashboard_*.json to dashboard folder. if the file cbpi_dashboard_1.json doesnt exist in the dashboard folder every cbpi_dashboard_*.json file from the config folder is moved to the dashboard folder. This also removes the empty default cbpi_dashboard_1.json file from the template folder, because the file is not needed for cbpi to function and can be created when editing the dashboard online. --- .../dashboard/cbpi_dashboard_1.json | 3 --- cbpi/config/dashboard/cbpi_dashboard_1.json | 3 --- cbpi/configFolder.py | 11 +++++++++-- 3 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .devcontainer/cbpi-default-dev-config/dashboard/cbpi_dashboard_1.json delete mode 100644 cbpi/config/dashboard/cbpi_dashboard_1.json diff --git a/.devcontainer/cbpi-default-dev-config/dashboard/cbpi_dashboard_1.json b/.devcontainer/cbpi-default-dev-config/dashboard/cbpi_dashboard_1.json deleted file mode 100644 index 92079a0..0000000 --- a/.devcontainer/cbpi-default-dev-config/dashboard/cbpi_dashboard_1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "elements": [] -} \ No newline at end of file diff --git a/cbpi/config/dashboard/cbpi_dashboard_1.json b/cbpi/config/dashboard/cbpi_dashboard_1.json deleted file mode 100644 index 92079a0..0000000 --- a/cbpi/config/dashboard/cbpi_dashboard_1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "elements": [] -} \ No newline at end of file diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index ef2169b..b35d4a1 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -96,13 +96,13 @@ class ConfigFolder: ['config.json', 'file'], ['craftbeerpi.service', 'file'], ['chromium.desktop', 'file'], - ['dashboard/cbpi_dashboard_1.json', 'file'], - ['dashboard/widgets', 'folder'], ['dashboard', 'folder'], + ['dashboard/widgets', 'folder'], ['fermenterrecipes', 'folder'], [self.logsFolderPath, 'folder'], ['recipes', 'folder'], ['upload', 'folder'] + #['dashboard/cbpi_dashboard_1.json', 'file'] no need to check - can be created with online editor ] for checking in required_config_content: if self.inform_missing_content(self.check_for_file_or_folder(os.path.join(self.configFolderPath, checking[0]), checking[1])): @@ -117,6 +117,13 @@ class ConfigFolder: print("of course you can also place your config files manually") print("***************************************************") return False + + # if cbpi_dashboard_1.json doesnt exist at the new location (configFolderPath/dashboard) + # we move every cbpi_dashboard_n.json file from the old location (configFolderPath) there. + # this could be a config zip file restore from version 4.0.7.a4 or prior. + if not (os.path.isfile(os.path.join(self.configFolderPath, 'dashboard', 'cbpi_dashboard_1.json'))): + for file in glob.glob(os.path.join(self.configFolderPath, 'cbpi_dashboard_*.json')): + shutil.move(file, os.path.join(self.configFolderPath, 'dashboard', os.path.basename(file))) def inform_missing_content(self, whatsmissing : str): if whatsmissing == "": From 7f16a8b9b55e8da58281abb697f0710f505719cf Mon Sep 17 00:00:00 2001 From: prash3r Date: Sun, 2 Oct 2022 19:01:14 +0200 Subject: [PATCH 33/48] not copying empty cbpi_dashboard_1.json anymore. will be created on first dashboard save instead. --- cbpi/configFolder.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index b35d4a1..be01fc0 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -170,11 +170,6 @@ class ConfigFolder: self.copyDefaultFileIfNotExists("craftbeerpi.service") self.copyDefaultFileIfNotExists("chromium.desktop") - if os.path.exists(os.path.join(self.configFolderPath, "dashboard", "cbpi_dashboard_1.json")) is False: - srcfile = os.path.join(os.path.dirname(__file__), "config", "dashboard", "cbpi_dashboard_1.json") - destfile = os.path.join(self.configFolderPath, "dashboard") - shutil.copy(srcfile, destfile) - print("Config Folder created") def create_home_folder_structure(configFolder): From 92ddb2a45a4e1e98beee45d2de8ec03ff3d414d2 Mon Sep 17 00:00:00 2001 From: Alexander Vollkopf <43980694+avollkopf@users.noreply.github.com> Date: Mon, 3 Oct 2022 12:04:42 +0200 Subject: [PATCH 34/48] Revert "add hop name to BoilStep and UploadController" --- cbpi/controller/upload_controller.py | 130 ++++++++++----------------- cbpi/extension/mashstep/__init__.py | 24 ++--- 2 files changed, 52 insertions(+), 102 deletions(-) diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index 5faccb3..92aec4f 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -193,7 +193,6 @@ class UploadController: pass # get the hop addition times - c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze <> 1 AND SudID = ?', (Recipe_ID,)) hops = c.fetchall() whirlpool = [] @@ -204,11 +203,11 @@ class UploadController: hops.remove(whirl) # get the misc addition times - c.execute('SELECT Zugabedauer, Name FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zugabedauer FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) miscs = c.fetchall() try: - c.execute('SELECT Zeit, Name FROM Hopfengaben WHERE Vorderwuerze = 1 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze = 1 AND SudID = ?', (Recipe_ID,)) FW_Hops = c.fetchall() FirstWort = self.getFirstWort(FW_Hops,"kbh") except: @@ -298,21 +297,14 @@ class UploadController: "Sensor": self.boilkettle.sensor, "Temp": int(self.BoilTemp), "Timer": BoilTime, - "First_Wort": FirstWort[0], - "First_Wort_text": FirstWort[1], + "First_Wort": FirstWort, "LidAlert": "Yes", - "Hop_1": Hops[0][0], - "Hop_1_text": Hops[0][1], - "Hop_2": Hops[1][0], - "Hop_2_text": Hops[1][1], - "Hop_3": Hops[2][0], - "Hop_3_text": Hops[2][1], - "Hop_4": Hops[3][0], - "Hop_4_text": Hops[3][1], - "Hop_5": Hops[4][0], - "Hop_5_text": Hops[4][1], - "Hop_6": Hops[5][0], - "Hop_6_text": Hops[5][1] + "Hop_1": Hops[0], + "Hop_2": Hops[1], + "Hop_3": Hops[2], + "Hop_4": Hops[3], + "Hop_5": Hops[4], + "Hop_6": Hops[5] }, "status_text": "", "status": "I", @@ -374,7 +366,7 @@ class UploadController: elif e["Hopfen_{}_Kochzeit".format(idx)] == "Whirlpool": alert = float(1) else: - self.cbpi.notify("No Number at Hoptime", "Please change json-File at Hopfen_{}_Kochzeit".format(idx), NotificationType.ERROR) + self.api.notify(headline="No Number at Hoptime", message="Please change json-File at Hopfen_{}_Kochzeit".format(idx), type="danger") alert = float(1) hops.append({"name":hops_name,"time":alert}) @@ -527,21 +519,14 @@ class UploadController: "Sensor": sensor, "Temp": step_temp, "Timer": step_time, - "First_Wort": FirstWort[0], - "First_Wort_text": FirstWort[1], + "First_Wort": FirstWort, "LidAlert": LidAlert, - "Hop_1": Hops[0][0], - "Hop_1_text": Hops[0][1], - "Hop_2": Hops[1][0], - "Hop_2_text": Hops[1][1], - "Hop_3": Hops[2][0], - "Hop_3_text": Hops[2][1], - "Hop_4": Hops[3][0], - "Hop_4_text": Hops[3][1], - "Hop_5": Hops[4][0], - "Hop_5_text": Hops[4][1], - "Hop_6": Hops[5][0], - "Hop_6_text": Hops[5][1] + "Hop_1": Hops[0], + "Hop_2": Hops[1], + "Hop_3": Hops[2], + "Hop_4": Hops[3], + "Hop_5": Hops[4], + "Hop_6": Hops[5] }, "status_text": "", "status": "I", @@ -680,21 +665,14 @@ class UploadController: "Sensor": sensor, "Temp": step_temp, "Timer": step_time, - "First_Wort": FirstWort[0], - "First_Wort_text": FirstWort[1], + "First_Wort": FirstWort, "LidAlert": LidAlert, - "Hop_1": Hops[0][0], - "Hop_1_text": Hops[0][1], - "Hop_2": Hops[1][0], - "Hop_2_text": Hops[1][1], - "Hop_3": Hops[2][0], - "Hop_3_text": Hops[2][1], - "Hop_4": Hops[3][0], - "Hop_4_text": Hops[3][1], - "Hop_5": Hops[4][0], - "Hop_5_text": Hops[4][1], - "Hop_6": Hops[5][0], - "Hop_6_text": Hops[5][1] + "Hop_1": Hops[0], + "Hop_2": Hops[1], + "Hop_3": Hops[2], + "Hop_4": Hops[3], + "Hop_5": Hops[4], + "Hop_6": Hops[5] }, "status_text": "", "status": "I", @@ -889,21 +867,14 @@ class UploadController: "Sensor": sensor, "Temp": step_temp, "Timer": step_time, - "First_Wort": FirstWort[0], - "First_Wort_text": FirstWort[1], + "First_Wort": FirstWort, "LidAlert": LidAlert, - "Hop_1": Hops[0][0], - "Hop_1_text": Hops[0][1], - "Hop_2": Hops[1][0], - "Hop_2_text": Hops[1][1], - "Hop_3": Hops[2][0], - "Hop_3_text": Hops[2][1], - "Hop_4": Hops[3][0], - "Hop_4_text": Hops[3][1], - "Hop_5": Hops[4][0], - "Hop_5_text": Hops[4][1], - "Hop_6": Hops[5][0], - "Hop_6_text": Hops[5][1] + "Hop_1": Hops[0], + "Hop_2": Hops[1], + "Hop_3": Hops[2], + "Hop_4": Hops[3], + "Hop_5": Hops[4], + "Hop_6": Hops[5] }, "status_text": "", "status": "I", @@ -926,50 +897,48 @@ class UploadController: ## Hops which are not used in the boil step should not cause alerts if use != 'Aroma' and use != 'Boil': continue - alerts.append([float(hop.find('TIME').text), hop.find('NAME').text]) + alerts.append(float(hop.find('TIME').text)) elif recipe_type == "bf": use = hop['use'] if use != 'Aroma' and use != 'Boil': continue - alerts.append([float(hop['time']), hop['name']]) ## TODO: Testing + alerts.append(float(hop['time'])) elif recipe_type == "kbh": - alerts.append([float(hop[0]), hop[1]]) + alerts.append(float(hop[0])) elif recipe_type == "json": - alerts.append([float(hop['time']), hop['name']]) + alerts.append(float(hop['time'])) ## There might also be miscelaneous additions during boild time if miscs is not None: for misc in miscs: if recipe_type == "xml": - alerts.append([float(misc.find('TIME').text), misc.find('NAME').text]) + alerts.append(float(misc.find('TIME').text)) elif recipe_type == "bf": use = misc['use'] if use != 'Aroma' and use != 'Boil': continue - alerts.append([float(misc['time']), misc['name']]) ## TODO: Testing + alerts.append(float(misc['time'])) elif recipe_type == "kbh": - alerts.append([float(misc[0]), misc[1]]) + alerts.append(float(misc[0])) elif recipe_type == "json": - alerts.append([float(misc['time']), misc['name']]) - ## Dedupe and order the additions by their time + alerts.append(float(misc['time'])) + ## Dedupe and order the additions by their time, to prevent multiple alerts at the same time + alerts = sorted(list(set(alerts))) ## CBP should have these additions in reverse - alerts = sorted(alerts, key=lambda x:x[0], reverse=True) + alerts.reverse() hop_alerts = [] for i in range(0,6): try: - hop_alerts.append(alerts[i]) + hop_alerts.append(str(int(alerts[i]))) except: - hop_alerts.append([None, None]) + hop_alerts.append(None) return hop_alerts def getFirstWort(self, hops, recipe_type): alert = "No" - names = [] if recipe_type == "kbh": if len(hops) != 0: alert = "Yes" - for hop in hops: - names.append(hop[1]) elif recipe_type == "xml": for hop in hops: use = hop.find('USE').text @@ -977,19 +946,14 @@ class UploadController: if use != 'First Wort': continue alert = "Yes" - names.append(hop.find('NAME').text) elif recipe_type == "bf": for hop in hops: if hop['use'] == "First Wort": alert="Yes" - names.append(hop['name']) ## TODO: Testing elif recipe_type == "json": - if len(hops) != 0: - alert = "Yes" - for hop in hops: - names.append(hop['name']) - - return [alert, " and ".join(names)] + for hop in hops: + alert="Yes" + return alert async def create_Whirlpool_Cooldown(self, time : str = "15"): # Add Waitstep as Whirlpool diff --git a/cbpi/extension/mashstep/__init__.py b/cbpi/extension/mashstep/__init__.py index 2feeabc..c830de2 100644 --- a/cbpi/extension/mashstep/__init__.py +++ b/cbpi/extension/mashstep/__init__.py @@ -322,19 +322,12 @@ class ActorStep(CBPiStep): Property.Select(label="LidAlert",options=["Yes","No"], description="Trigger Alert to remove lid if temp is close to boil"), Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Kettlelogic automatically on and off -> Yes"), Property.Select("First_Wort", options=["Yes","No"], description="First Wort Hop alert if set to Yes"), - Property.Text("First_Wort_text", configurable = True, description="First Wort Hop alert text"), Property.Number("Hop_1", configurable = True, description="First Hop alert (minutes before finish)"), - Property.Text("Hop_1_text", configurable = True, description="First Hop alert text"), Property.Number("Hop_2", configurable=True, description="Second Hop alert (minutes before finish)"), - Property.Text("Hop_2_text", configurable = True, description="Second Hop alert text"), Property.Number("Hop_3", configurable=True, description="Third Hop alert (minutes before finish)"), - Property.Text("Hop_3_text", configurable = True, description="Third Hop alert text"), Property.Number("Hop_4", configurable=True, description="Fourth Hop alert (minutes before finish)"), - Property.Text("Hop_4_text", configurable = True, description="Fourth Hop alert text"), Property.Number("Hop_5", configurable=True, description="Fifth Hop alert (minutes before finish)"), - Property.Text("Hop_5_text", configurable = True, description="Fifth Hop alert text"), - Property.Number("Hop_6", configurable=True, description="Sixth Hop alert (minutes before finish)"), - Property.Text("Hop_6_text", configurable = True, description="Sixth Hop alert text")]) + Property.Number("Hop_6", configurable=True, description="Sixth Hop alert (minutes before finish)")]) class BoilStep(CBPiStep): @action("Start Timer", []) @@ -374,7 +367,6 @@ class BoilStep(CBPiStep): self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False self.first_wort_hop_flag = False self.first_wort_hop=self.props.get("First_Wort", "No") - self.first_wort_hop_text=self.props.get("First_Wort_text", None) self.hops_added=["","","","","",""] self.remaining_seconds = None @@ -397,14 +389,11 @@ class BoilStep(CBPiStep): await self.setAutoMode(True) await self.push_update() - async def check_hop_timer(self, number, value, text): + async def check_hop_timer(self, number, value): if value is not None and self.hops_added[number-1] is not True: if self.remaining_seconds != None and self.remaining_seconds <= (int(value) * 60 + 1): self.hops_added[number-1]= True - if text is not None and text != "": - self.cbpi.notify('Hop Alert', "Please add %s (%s)" % (text, number), NotificationType.INFO) - else: - self.cbpi.notify('Hop Alert', "Please add Hop %s" % number, NotificationType.INFO) + self.cbpi.notify('Hop Alert', "Please add Hop %s" % number, NotificationType.INFO) async def on_stop(self): await self.timer.stop() @@ -420,10 +409,7 @@ class BoilStep(CBPiStep): async def run(self): if self.first_wort_hop_flag == False and self.first_wort_hop == "Yes": self.first_wort_hop_flag = True - if self.first_wort_hop_text is not None and self.first_wort_hop_text != "": - self.cbpi.notify('First Wort Hop Addition!', 'Please add %s for first wort' % self.first_wort_hop_text, NotificationType.INFO) - else: - self.cbpi.notify('First Wort Hop Addition!', 'Please add hops for first wort', NotificationType.INFO) + self.cbpi.notify('First Wort Hop Addition!', 'Please add hops for first wort', NotificationType.INFO) while self.running == True: await asyncio.sleep(1) @@ -440,7 +426,7 @@ class BoilStep(CBPiStep): self.cbpi.notify(self.name, 'Timer started. Estimated completion: {}'.format(estimated_completion_time.strftime("%H:%M")), NotificationType.INFO) else: for x in range(1, 6): - await self.check_hop_timer(x, self.props.get("Hop_%s" % x, None), self.props.get("Hop_%s_text" % x, None)) + await self.check_hop_timer(x, self.props.get("Hop_%s" % x, None)) return StepResult.DONE From 21ce9ed220ea16fe83b01d8a15f2fff9e54c3f72 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Mon, 3 Oct 2022 12:11:53 +0200 Subject: [PATCH 35/48] update version number --- cbpi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index d49370d..8a5d341 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a4" +__version__ = "4.0.7.a5" __codename__ = "Spring Break" From 2ebc659bcf6e70ebba7f01fc8b594c55b5e12efc Mon Sep 17 00:00:00 2001 From: lopelex Date: Mon, 3 Oct 2022 16:28:42 +0200 Subject: [PATCH 36/48] fix conflicts with #62 --- cbpi/controller/upload_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index 5faccb3..d5c15d4 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -194,7 +194,7 @@ class UploadController: # get the hop addition times - c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze <> 1 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zeit, Name FROM Hopfengaben WHERE Vorderwuerze <> 1 AND SudID = ?', (Recipe_ID,)) hops = c.fetchall() whirlpool = [] for hop in hops: From 17a2d403bfc172306817862f9e168746281d2863 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sat, 22 Oct 2022 13:02:13 +0200 Subject: [PATCH 37/48] added missing updated for boilstep --- cbpi/extension/mashstep/__init__.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/cbpi/extension/mashstep/__init__.py b/cbpi/extension/mashstep/__init__.py index c830de2..2feeabc 100644 --- a/cbpi/extension/mashstep/__init__.py +++ b/cbpi/extension/mashstep/__init__.py @@ -322,12 +322,19 @@ class ActorStep(CBPiStep): Property.Select(label="LidAlert",options=["Yes","No"], description="Trigger Alert to remove lid if temp is close to boil"), Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Kettlelogic automatically on and off -> Yes"), Property.Select("First_Wort", options=["Yes","No"], description="First Wort Hop alert if set to Yes"), + Property.Text("First_Wort_text", configurable = True, description="First Wort Hop alert text"), Property.Number("Hop_1", configurable = True, description="First Hop alert (minutes before finish)"), + Property.Text("Hop_1_text", configurable = True, description="First Hop alert text"), Property.Number("Hop_2", configurable=True, description="Second Hop alert (minutes before finish)"), + Property.Text("Hop_2_text", configurable = True, description="Second Hop alert text"), Property.Number("Hop_3", configurable=True, description="Third Hop alert (minutes before finish)"), + Property.Text("Hop_3_text", configurable = True, description="Third Hop alert text"), Property.Number("Hop_4", configurable=True, description="Fourth Hop alert (minutes before finish)"), + Property.Text("Hop_4_text", configurable = True, description="Fourth Hop alert text"), Property.Number("Hop_5", configurable=True, description="Fifth Hop alert (minutes before finish)"), - Property.Number("Hop_6", configurable=True, description="Sixth Hop alert (minutes before finish)")]) + Property.Text("Hop_5_text", configurable = True, description="Fifth Hop alert text"), + Property.Number("Hop_6", configurable=True, description="Sixth Hop alert (minutes before finish)"), + Property.Text("Hop_6_text", configurable = True, description="Sixth Hop alert text")]) class BoilStep(CBPiStep): @action("Start Timer", []) @@ -367,6 +374,7 @@ class BoilStep(CBPiStep): self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False self.first_wort_hop_flag = False self.first_wort_hop=self.props.get("First_Wort", "No") + self.first_wort_hop_text=self.props.get("First_Wort_text", None) self.hops_added=["","","","","",""] self.remaining_seconds = None @@ -389,11 +397,14 @@ class BoilStep(CBPiStep): await self.setAutoMode(True) await self.push_update() - async def check_hop_timer(self, number, value): + async def check_hop_timer(self, number, value, text): if value is not None and self.hops_added[number-1] is not True: if self.remaining_seconds != None and self.remaining_seconds <= (int(value) * 60 + 1): self.hops_added[number-1]= True - self.cbpi.notify('Hop Alert', "Please add Hop %s" % number, NotificationType.INFO) + if text is not None and text != "": + self.cbpi.notify('Hop Alert', "Please add %s (%s)" % (text, number), NotificationType.INFO) + else: + self.cbpi.notify('Hop Alert', "Please add Hop %s" % number, NotificationType.INFO) async def on_stop(self): await self.timer.stop() @@ -409,7 +420,10 @@ class BoilStep(CBPiStep): async def run(self): if self.first_wort_hop_flag == False and self.first_wort_hop == "Yes": self.first_wort_hop_flag = True - self.cbpi.notify('First Wort Hop Addition!', 'Please add hops for first wort', NotificationType.INFO) + if self.first_wort_hop_text is not None and self.first_wort_hop_text != "": + self.cbpi.notify('First Wort Hop Addition!', 'Please add %s for first wort' % self.first_wort_hop_text, NotificationType.INFO) + else: + self.cbpi.notify('First Wort Hop Addition!', 'Please add hops for first wort', NotificationType.INFO) while self.running == True: await asyncio.sleep(1) @@ -426,7 +440,7 @@ class BoilStep(CBPiStep): self.cbpi.notify(self.name, 'Timer started. Estimated completion: {}'.format(estimated_completion_time.strftime("%H:%M")), NotificationType.INFO) else: for x in range(1, 6): - await self.check_hop_timer(x, self.props.get("Hop_%s" % x, None)) + await self.check_hop_timer(x, self.props.get("Hop_%s" % x, None), self.props.get("Hop_%s_text" % x, None)) return StepResult.DONE From 8b2e0fb1d8469cd98ab15e1178ca8767c72fe63d Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sat, 22 Oct 2022 14:08:59 +0200 Subject: [PATCH 38/48] avoid error logging / notification for Notification step w/o Sensor definittion --- cbpi/__init__.py | 2 +- cbpi/controller/sensor_controller.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 8a5d341..490799a 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a5" +__version__ = "4.0.7.a6" __codename__ = "Spring Break" diff --git a/cbpi/controller/sensor_controller.py b/cbpi/controller/sensor_controller.py index 6c70c87..ae8278f 100644 --- a/cbpi/controller/sensor_controller.py +++ b/cbpi/controller/sensor_controller.py @@ -19,6 +19,8 @@ class SensorController(BasicController): return dict(name=data.get("name"), id=data.get("id"), type=data.get("type"), state=state,props=data.get("props", [])) def get_sensor_value(self, id): + if id is None: + return None try: return self.find_by_id(id).instance.get_state() except Exception as e: From 4e234f475326bc2dbeb7cb85d5dab00a7e77ef25 Mon Sep 17 00:00:00 2001 From: lopelex Date: Sat, 5 Nov 2022 12:57:44 +0100 Subject: [PATCH 39/48] add hop text --- cbpi/controller/upload_controller.py | 146 +++++++++++++++++---------- 1 file changed, 92 insertions(+), 54 deletions(-) diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index 7fa0cd3..e2828a2 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -203,11 +203,11 @@ class UploadController: hops.remove(whirl) # get the misc addition times - c.execute('SELECT Zugabedauer FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zugabedauer, Name FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) miscs = c.fetchall() try: - c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze = 1 AND SudID = ?', (Recipe_ID,)) + c.execute('SELECT Zeit, Name FROM Hopfengaben WHERE Vorderwuerze = 1 AND SudID = ?', (Recipe_ID,)) FW_Hops = c.fetchall() FirstWort = self.getFirstWort(FW_Hops,"kbh") except: @@ -297,14 +297,21 @@ class UploadController: "Sensor": self.boilkettle.sensor, "Temp": int(self.BoilTemp), "Timer": BoilTime, - "First_Wort": FirstWort, + "First_Wort": FirstWort[0], + "First_Wort_text": FirstWort[1], "LidAlert": "Yes", - "Hop_1": Hops[0], - "Hop_2": Hops[1], - "Hop_3": Hops[2], - "Hop_4": Hops[3], - "Hop_5": Hops[4], - "Hop_6": Hops[5] + "Hop_1": Hops[0][0], + "Hop_1_text": Hops[0][1], + "Hop_2": Hops[1][0], + "Hop_2_text": Hops[1][1], + "Hop_3": Hops[2][0], + "Hop_3_text": Hops[2][1], + "Hop_4": Hops[3][0], + "Hop_4_text": Hops[3][1], + "Hop_5": Hops[4][0], + "Hop_5_text": Hops[4][1], + "Hop_6": Hops[5][0], + "Hop_6_text": Hops[5][1] }, "status_text": "", "status": "I", @@ -363,11 +370,12 @@ class UploadController: if e["Hopfen_{}_Kochzeit".format(idx)].isnumeric(): if boil_time is not e["Hopfen_{}_Kochzeit".format(idx)].isnumeric(): alert = float(e["Hopfen_{}_Kochzeit".format(idx)]) - elif e["Hopfen_{}_Kochzeit".format(idx)] == "Whirlpool": - alert = float(1) + elif e["Hopfen_{}_Kochzeit".format(idx)] == "Whirlpool" or float(e["Hopfen_{}_Kochzeit".format(idx)]) < 0: + alert = float(0) + hops_name = hops_name + ' whirlpool' else: - self.api.notify(headline="No Number at Hoptime", message="Please change json-File at Hopfen_{}_Kochzeit".format(idx), type="danger") - alert = float(1) + self.cbpi.notify("No Number at Hoptime", "Please change json-File at Hopfen_{}_Kochzeit".format(idx), NotificationType.ERROR) + alert = float(0) hops.append({"name":hops_name,"time":alert}) @@ -385,11 +393,12 @@ class UploadController: miscs_name = "%s%s %s" % (e["WeitereZutat_Wuerze_{}_Menge".format(idx)],e["WeitereZutat_Wuerze_{}_Einheit".format(idx)],e["WeitereZutat_Wuerze_{}_Name".format(idx)]) if e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)].isnumeric(): alert = float(e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)]) - elif e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)] == "Whirlpool": - alert = float(1) + elif e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)] == "Whirlpool" or float(e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)]) < 0: + alert = float(0) + miscs_name = miscs_name + ' whirlpool' else: self.api.notify(headline="No Number at Hoptime", message="Please change json-File at WeitereZutat_Wuerze_{}_Kochzeit".format(idx), type="danger") - alert = float(1) + alert = float(0) miscs.append({"name":miscs_name,"time":alert}) @@ -519,14 +528,21 @@ class UploadController: "Sensor": sensor, "Temp": step_temp, "Timer": step_time, - "First_Wort": FirstWort, + "First_Wort": FirstWort[0], + "First_Wort_text": FirstWort[1], "LidAlert": LidAlert, - "Hop_1": Hops[0], - "Hop_2": Hops[1], - "Hop_3": Hops[2], - "Hop_4": Hops[3], - "Hop_5": Hops[4], - "Hop_6": Hops[5] + "Hop_1": Hops[0][0], + "Hop_1_text": Hops[0][1], + "Hop_2": Hops[1][0], + "Hop_2_text": Hops[1][1], + "Hop_3": Hops[2][0], + "Hop_3_text": Hops[2][1], + "Hop_4": Hops[3][0], + "Hop_4_text": Hops[3][1], + "Hop_5": Hops[4][0], + "Hop_5_text": Hops[4][1], + "Hop_6": Hops[5][0], + "Hop_6_text": Hops[5][1] }, "status_text": "", "status": "I", @@ -665,14 +681,21 @@ class UploadController: "Sensor": sensor, "Temp": step_temp, "Timer": step_time, - "First_Wort": FirstWort, + "First_Wort": FirstWort[0], + "First_Wort_text": FirstWort[1], "LidAlert": LidAlert, - "Hop_1": Hops[0], - "Hop_2": Hops[1], - "Hop_3": Hops[2], - "Hop_4": Hops[3], - "Hop_5": Hops[4], - "Hop_6": Hops[5] + "Hop_1": Hops[0][0], + "Hop_1_text": Hops[0][1], + "Hop_2": Hops[1][0], + "Hop_2_text": Hops[1][1], + "Hop_3": Hops[2][0], + "Hop_3_text": Hops[2][1], + "Hop_4": Hops[3][0], + "Hop_4_text": Hops[3][1], + "Hop_5": Hops[4][0], + "Hop_5_text": Hops[4][1], + "Hop_6": Hops[5][0], + "Hop_6_text": Hops[5][1] }, "status_text": "", "status": "I", @@ -867,14 +890,21 @@ class UploadController: "Sensor": sensor, "Temp": step_temp, "Timer": step_time, - "First_Wort": FirstWort, + "First_Wort": FirstWort[0], + "First_Wort_text": FirstWort[1], "LidAlert": LidAlert, - "Hop_1": Hops[0], - "Hop_2": Hops[1], - "Hop_3": Hops[2], - "Hop_4": Hops[3], - "Hop_5": Hops[4], - "Hop_6": Hops[5] + "Hop_1": Hops[0][0], + "Hop_1_text": Hops[0][1], + "Hop_2": Hops[1][0], + "Hop_2_text": Hops[1][1], + "Hop_3": Hops[2][0], + "Hop_3_text": Hops[2][1], + "Hop_4": Hops[3][0], + "Hop_4_text": Hops[3][1], + "Hop_5": Hops[4][0], + "Hop_5_text": Hops[4][1], + "Hop_6": Hops[5][0], + "Hop_6_text": Hops[5][1] }, "status_text": "", "status": "I", @@ -897,48 +927,51 @@ class UploadController: ## Hops which are not used in the boil step should not cause alerts if use != 'Aroma' and use != 'Boil': continue - alerts.append(float(hop.find('TIME').text)) + alerts.append([float(hop.find('TIME').text), hop.find('NAME').text]) elif recipe_type == "bf": use = hop['use'] if use != 'Aroma' and use != 'Boil': continue - alerts.append(float(hop['time'])) + alerts.append([float(hop['time']), hop['name']]) ## TODO: Testing elif recipe_type == "kbh": - alerts.append(float(hop[0])) + alerts.append([float(hop[0]), hop[1]]) elif recipe_type == "json": - alerts.append(float(hop['time'])) + alerts.append([float(hop['time']), hop['name']]) ## There might also be miscelaneous additions during boild time if miscs is not None: for misc in miscs: if recipe_type == "xml": - alerts.append(float(misc.find('TIME').text)) + alerts.append([float(misc.find('TIME').text), misc.find('NAME').text]) elif recipe_type == "bf": use = misc['use'] if use != 'Aroma' and use != 'Boil': continue - alerts.append(float(misc['time'])) + alerts.append([float(misc['time']), misc['name']]) ## TODO: Testing elif recipe_type == "kbh": - alerts.append(float(misc[0])) + alerts.append([float(misc[0]), misc[1]]) elif recipe_type == "json": - alerts.append(float(misc['time'])) - ## Dedupe and order the additions by their time, to prevent multiple alerts at the same time - alerts = sorted(list(set(alerts))) + alerts.append([float(misc['time']), misc['name']]) + ## Dedupe and order the additions by their time ## CBP should have these additions in reverse - alerts.reverse() - hop_alerts = [] + alerts = sorted(alerts, key=lambda x:x[0], reverse=True) + hop_alerts = [[None, None],[None, None],[None, None],[None, None],[None, None],[None, None]] for i in range(0,6): try: - hop_alerts.append(str(int(alerts[i]))) + if float(alerts[i][0]) > -1: + hop_alerts[i] = alerts[i] except: - hop_alerts.append(None) + pass return hop_alerts def getFirstWort(self, hops, recipe_type): alert = "No" + names = [] if recipe_type == "kbh": if len(hops) != 0: alert = "Yes" + for hop in hops: + names.append(hop[1]) elif recipe_type == "xml": for hop in hops: use = hop.find('USE').text @@ -946,14 +979,19 @@ class UploadController: if use != 'First Wort': continue alert = "Yes" + names.append(hop.find('NAME').text) elif recipe_type == "bf": for hop in hops: if hop['use'] == "First Wort": alert="Yes" + names.append(hop['name']) ## TODO: Testing elif recipe_type == "json": - for hop in hops: - alert="Yes" - return alert + if len(hops) != 0: + alert = "Yes" + for hop in hops: + names.append(hop['name']) + + return [alert, " and ".join(names)] async def create_Whirlpool_Cooldown(self, time : str = "15"): # Add Waitstep as Whirlpool From f38697df936253904077f46214f7c8c41eebcf0d Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Thu, 17 Nov 2022 19:40:20 +0100 Subject: [PATCH 40/48] update requirements --- requirements.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 0c49857..674e3bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +typing-extensions>=4 aiohttp==3.8.1 aiohttp-auth==0.1.1 aiohttp-route-decorator==0.1.4 diff --git a/setup.py b/setup.py index 07cb51a..a356451 100644 --- a/setup.py +++ b/setup.py @@ -38,6 +38,7 @@ setup(name='cbpi4', long_description=long_description, long_description_content_type='text/markdown', install_requires=[ + "typing-extensions>=4", "aiohttp==3.8.1", "aiohttp-auth==0.1.1", "aiohttp-route-decorator==0.1.4", From afeba817403bdd737c9c53f60397384ef25b6c5a Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Thu, 17 Nov 2022 19:41:06 +0100 Subject: [PATCH 41/48] bump rev --- cbpi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 490799a..550ceeb 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a6" +__version__ = "4.0.7.a7" __codename__ = "Spring Break" From f83ee713699c4ecfdefad681563ccf34d61676a8 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sat, 19 Nov 2022 12:47:55 +0100 Subject: [PATCH 42/48] bump rev --- cbpi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 550ceeb..8bcc9ae 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a7" +__version__ = "4.0.7.a8" __codename__ = "Spring Break" From f13993db31a8ccb9a506eb98a8b9d673fd64b244 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 27 Nov 2022 10:49:59 +0100 Subject: [PATCH 43/48] removed loop from asyncio.queue (depracted since py 3.9 and removed in 3.10) --- cbpi/__init__.py | 2 +- cbpi/job/_scheduler.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 8bcc9ae..54abc01 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a8" +__version__ = "4.0.7.a9" __codename__ = "Spring Break" diff --git a/cbpi/job/_scheduler.py b/cbpi/job/_scheduler.py index 28db254..9393759 100644 --- a/cbpi/job/_scheduler.py +++ b/cbpi/job/_scheduler.py @@ -21,9 +21,9 @@ class Scheduler(*bases): self._close_timeout = close_timeout self._limit = limit self._exception_handler = exception_handler - self._failed_tasks = asyncio.Queue(loop=loop) + self._failed_tasks = asyncio.Queue() self._failed_task = loop.create_task(self._wait_failed()) - self._pending = asyncio.Queue(maxsize=pending_limit, loop=loop) + self._pending = asyncio.Queue(maxsize=pending_limit) self._closed = False def __iter__(self): From 05b70e3cd7cb9ccd4efc8dbfbfe2d5eac9012eae Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 27 Nov 2022 15:16:18 +0100 Subject: [PATCH 44/48] Fix log level setting for cli --- cbpi/__init__.py | 4 ++-- cbpi/cli.py | 3 ++- cbpi/craftbeerpi.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 54abc01..94c2aac 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a9" -__codename__ = "Spring Break" +__version__ = "4.0.7.a10" +__codename__ = "November Rain" diff --git a/cbpi/cli.py b/cbpi/cli.py index bc3b13d..a0d783e 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -239,8 +239,9 @@ def main(context, config_folder_path, logs_folder_path, debug_log_level): if logs_folder_path == "": logs_folder_path = os.path.join(Path(config_folder_path).absolute().parent, 'logs') formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s') - logging.basicConfig(format=formatter,level=debug_log_level, stream=logging.StreamHandler()) + logging.basicConfig(format=formatter, stream=logging.StreamHandler()) logger = logging.getLogger() + logger.setLevel(debug_log_level) try: if not os.path.isdir(logs_folder_path): logger.info(f"logs folder '{logs_folder_path}' doesnt exist and we are trying to create it") diff --git a/cbpi/craftbeerpi.py b/cbpi/craftbeerpi.py index d3c60c6..975a91a 100644 --- a/cbpi/craftbeerpi.py +++ b/cbpi/craftbeerpi.py @@ -297,7 +297,7 @@ class CraftBeerPi: await self.kettle.init() await self.call_initializer(self.app) await self.dashboard.init() - + self._swagger_setup() From 7e0eb0f30d9a1a68e232a7cb1d441b93d3995d4a Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 27 Nov 2022 16:08:11 +0100 Subject: [PATCH 45/48] bump rev --- cbpi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 94c2aac..df87e13 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.a10" +__version__ = "4.0.7.rc1" __codename__ = "November Rain" From 9f655c9393b3145a059dceb8467cfb2fe066f399 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Fri, 2 Dec 2022 20:05:30 +0100 Subject: [PATCH 46/48] Add parameter for Notify on Error --- cbpi/__init__.py | 2 +- cbpi/controller/notification_controller.py | 5 ++++- cbpi/extension/ConfigUpdate/__init__.py | 13 ++++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index df87e13..2260e28 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.rc1" +__version__ = "4.0.7.rc2" __codename__ = "November Rain" diff --git a/cbpi/controller/notification_controller.py b/cbpi/controller/notification_controller.py index 26c25b4..33e6fb1 100644 --- a/cbpi/controller/notification_controller.py +++ b/cbpi/controller/notification_controller.py @@ -1,6 +1,7 @@ import asyncio from email import message from cbpi.api.dataclasses import NotificationType +from cbpi.api import * import logging import shortuuid class NotificationController: @@ -11,7 +12,9 @@ class NotificationController: ''' self.cbpi = cbpi self.logger = logging.getLogger(__name__) - logging.root.addFilter(self.notify_log_event) + NOTIFY_ON_ERROR = self.cbpi.config.get("NOTIFY_ON_ERROR", "No") + if NOTIFY_ON_ERROR == "Yes": + logging.root.addFilter(self.notify_log_event) self.callback_cache = {} self.listener = {} diff --git a/cbpi/extension/ConfigUpdate/__init__.py b/cbpi/extension/ConfigUpdate/__init__.py index 80808c3..4b4de2a 100644 --- a/cbpi/extension/ConfigUpdate/__init__.py +++ b/cbpi/extension/ConfigUpdate/__init__.py @@ -50,6 +50,7 @@ class ConfigUpdate(CBPiExtension): SENSOR_LOG_BACKUP_COUNT = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", None) SENSOR_LOG_MAX_BYTES = self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", None) slow_pipe_animation = self.cbpi.config.get("slow_pipe_animation", None) + NOTIFY_ON_ERROR = self.cbpi.config.get("NOTIFY_ON_ERROR", None) if boil_temp is None: logger.info("INIT Boil Temp Setting") @@ -313,7 +314,17 @@ class ConfigUpdate(CBPiExtension): {"label": "No", "value": "No"}]) except: logger.warning('Unable to update config') - + + ## Check if NOTIFY_ON_ERROR is in config + if NOTIFY_ON_ERROR is None: + logger.info("INIT NOTIFY_ON_ERROR") + try: + await self.cbpi.config.add("NOTIFY_ON_ERROR", "No", ConfigType.SELECT, "Send Notification on Logging Error", + [{"label": "Yes", "value": "Yes"}, + {"label": "No", "value": "No"}]) + except: + logger.warning('Unable to update config') + def setup(cbpi): cbpi.plugin.register("ConfigUpdate", ConfigUpdate) pass From cde20f647d9c54646c924f882f21fc45c3934896 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Fri, 2 Dec 2022 20:25:27 +0100 Subject: [PATCH 47/48] fix notify on error --- cbpi/__init__.py | 2 +- cbpi/controller/notification_controller.py | 29 +++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 2260e28..0618f27 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.rc2" +__version__ = "4.0.7.rc3" __codename__ = "November Rain" diff --git a/cbpi/controller/notification_controller.py b/cbpi/controller/notification_controller.py index 33e6fb1..eb105d6 100644 --- a/cbpi/controller/notification_controller.py +++ b/cbpi/controller/notification_controller.py @@ -12,24 +12,25 @@ class NotificationController: ''' self.cbpi = cbpi self.logger = logging.getLogger(__name__) - NOTIFY_ON_ERROR = self.cbpi.config.get("NOTIFY_ON_ERROR", "No") - if NOTIFY_ON_ERROR == "Yes": - logging.root.addFilter(self.notify_log_event) + logging.root.addFilter(self.notify_log_event) self.callback_cache = {} self.listener = {} def notify_log_event(self, record): - try: - if record.levelno > 20: - # on log events higher then INFO we want to notify all clients - type = NotificationType.WARNING - if record.levelno > 30: - type = NotificationType.ERROR - self.cbpi.notify(title=f"{record.levelname}", message=record.msg, type = type) - except Exception as e: - pass - finally: - return True + NOTIFY_ON_ERROR = self.cbpi.config.get("NOTIFY_ON_ERROR", "No") + if NOTIFY_ON_ERROR == "Yes": + try: + if record.levelno > 20: + # on log events higher then INFO we want to notify all clients + type = NotificationType.WARNING + if record.levelno > 30: + type = NotificationType.ERROR + self.cbpi.notify(title=f"{record.levelname}", message=record.msg, type = type) + except Exception as e: + pass + finally: + return True + return True def add_listener(self, method): listener_id = shortuuid.uuid() From 6a356c6addca810f28198b7ac1b6a2654a72c158 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sat, 10 Dec 2022 14:21:07 +0100 Subject: [PATCH 48/48] bump rev from rc to major --- cbpi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 0618f27..c4435e6 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7.rc3" +__version__ = "4.0.7" __codename__ = "November Rain"