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-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-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 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 96% rename from .devcontainer/cbpi-dev-config/config.json rename to .devcontainer/cbpi-default-dev-config/config.json index f6899f3..a94c57a 100644 --- a/.devcontainer/cbpi-dev-config/config.json +++ b/.devcontainer/cbpi-default-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/.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/widgets/_widgets_are_placed_here b/.devcontainer/cbpi-default-dev-config/dashboard/widgets/.gitkeep similarity index 100% rename from .devcontainer/cbpi-dev-config/dashboard/widgets/_widgets_are_placed_here 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/upload/_uploads_are_placed_here b/.devcontainer/cbpi-default-dev-config/fermenterrecipes/.gitkeep similarity index 100% rename from .devcontainer/cbpi-dev-config/upload/_uploads_are_placed_here 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-default-dev-config/recipes/.gitkeep b/.devcontainer/cbpi-default-dev-config/recipes/.gitkeep new file mode 100644 index 0000000..e69de29 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-default-dev-config/upload/.gitkeep b/.devcontainer/cbpi-default-dev-config/upload/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.devcontainer/cbpi-dev-config/dashboard/cbpi_dashboard_1.json b/.devcontainer/cbpi-dev-config/dashboard/cbpi_dashboard_1.json deleted file mode 100644 index 92079a0..0000000 --- a/.devcontainer/cbpi-dev-config/dashboard/cbpi_dashboard_1.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "elements": [] -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index bb723ac..32b01ba 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ node_modules .DS_Store config/* logs/ -.coverage \ No newline at end of file +.coverage +.devcontainer/cbpi-dev-config/* +cbpi4-* +temp* \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index c2f9637..e24e1e9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,11 +6,36 @@ "configurations": [ { - "name": "Run CraftBeerPi4", + "name": "run CraftBeerPi4", "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" + }, + + { + "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", + "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/.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/__init__.py b/cbpi/__init__.py index 2e6c461..c4435e6 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.6" -__codename__ = "Spring Break" +__version__ = "4.0.7" +__codename__ = "November Rain" diff --git a/cbpi/cli.py b/cbpi/cli.py index c1ff9be..a0d783e 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 @@ -8,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 @@ -15,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: @@ -85,7 +88,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 @@ -97,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 @@ -225,15 +230,29 @@ 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 - logging.basicConfig(level=level, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') - cbpi_cli = CraftBeerPiCli(ConfigFolder(config_folder_path)) + 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, 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") + 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/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 5741d4f..be01fc0 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 @@ -5,53 +6,43 @@ 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 + 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)) def get_file_path(self, file): - return os.path.join(self._rawPath, 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._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): - 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.configFolderPath, "restored_config.zip") if os.path.exists(os.path.join(backupfile)) is True: print("***************************************************") print("Found backup of config. Starting restore") @@ -68,14 +59,14 @@ 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": 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,17 +74,89 @@ 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'.") 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.configFolderPath, "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', '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])): + # 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.configFolderPath))) == 0 : + print("***************************************************") + 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") + print("cbpi4 to automatically unpack it") + 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 == "": + 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: 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): @@ -107,27 +170,27 @@ 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: - srcfile = os.path.join(os.path.dirname(__file__), "config", "dashboard", "cbpi_dashboard_1.json") - destfile = os.path.join(self._rawPath, "dashboard") - shutil.copy(srcfile, destfile) - 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") 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, 'recipes')).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(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) + pathlib.Path(os.path.join(self.configFolderPath, '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") 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/controller/config_controller.py b/cbpi/controller/config_controller.py index 4afaa0b..6d62d79 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.configFolderPath).absolute())) def get_state(self): diff --git a/cbpi/controller/dashboard_controller.py b/cbpi/controller/dashboard_controller.py index 228839c..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 @@ -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/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py index d835982..50790e2 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,24 +26,26 @@ class LogController: self.logger = logging.getLogger(__name__) self.configuration = False self.datalogger = {} + self.logsFolderPath = self.cbpi.config_folder.logsFolderPath + 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") self.influxdb = self.cbpi.config.get("INFLUXDB", "No") if self.logfiles == "Yes": if name not in self.datalogger: - max_bytes = self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 1048576) - backup_count = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3) + 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) + 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 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) @@ -115,8 +118,7 @@ class LogController: for name in names: # get all log names - all_filenames = glob.glob('./logs/sensor_%s.log*' % name) - + 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)) @@ -125,19 +127,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: @@ -146,7 +158,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(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() result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()} return result @@ -159,11 +176,10 @@ 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 glob.glob(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 = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*")) for f in all_filenames: os.remove(f) @@ -179,7 +195,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: """ @@ -188,7 +204,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) @@ -199,9 +215,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"{formatted_time}-sensor-{name}.zip") zip = zipfile.ZipFile(file_name, 'w', zipfile.ZIP_DEFLATED) - all_filenames = glob.glob('./logs/sensor_%s.log*' % name) + 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/cbpi/controller/notification_controller.py b/cbpi/controller/notification_controller.py index 5d2b4ef..eb105d6 100644 --- a/cbpi/controller/notification_controller.py +++ b/cbpi/controller/notification_controller.py @@ -1,5 +1,7 @@ import asyncio +from email import message from cbpi.api.dataclasses import NotificationType +from cbpi.api import * import logging import shortuuid class NotificationController: @@ -10,9 +12,26 @@ 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): + 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() self.listener[listener_id] = method 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: diff --git a/cbpi/controller/system_controller.py b/cbpi/controller/system_controller.py index 8c211fc..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)) @@ -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.configFolderPath, "restored_config.zip") + f=open(self.path, "wb") f.write(content) f.close() diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index 2d21de0..e2828a2 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -193,15 +193,21 @@ 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, Name 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) # 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: @@ -291,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", @@ -307,8 +320,11 @@ class UploadController: await self.create_step(step_string) - await self.create_Whirlpool_Cooldown() - + 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: @@ -354,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}) @@ -376,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}) @@ -510,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", @@ -656,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", @@ -858,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", @@ -888,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 @@ -937,22 +979,27 @@ 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): + 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 +1012,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 == '': diff --git a/cbpi/craftbeerpi.py b/cbpi/craftbeerpi.py index c4dad9d..975a91a 100644 --- a/cbpi/craftbeerpi.py +++ b/cbpi/craftbeerpi.py @@ -297,16 +297,10 @@ class CraftBeerPi: await self.kettle.init() await self.call_initializer(self.app) await self.dashboard.init() - + 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): diff --git a/cbpi/extension/ConfigUpdate/__init__.py b/cbpi/extension/ConfigUpdate/__init__.py index e5dc9ac..4b4de2a 100644 --- a/cbpi/extension/ConfigUpdate/__init__.py +++ b/cbpi/extension/ConfigUpdate/__init__.py @@ -47,8 +47,11 @@ 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) + 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") try: @@ -285,6 +288,42 @@ class ConfigUpdate(CBPiExtension): {"label": "PSI", "value": "PSI"}]) except: logger.warning('Unable to update config') + + # check if SENSOR_LOG_BACKUP_COUNT exists in config + if SENSOR_LOG_BACKUP_COUNT is None: + logger.info("INIT SENSOR_LOG_BACKUP_COUNT") + try: + await self.cbpi.config.add("SENSOR_LOG_BACKUP_COUNT", 3, ConfigType.NUMBER, "Max. number of backup logs") + except: + logger.warning('Unable to update database') + + # check if SENSOR_LOG_MAX_BYTES exists in config + 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 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') + + ## 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) 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 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 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 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): """ 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) 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) 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): 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): 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