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 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/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/cli.py b/cbpi/cli.py index c1ff9be..bc3b13d 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,28 @@ 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,level=debug_log_level, stream=logging.StreamHandler()) + logger = logging.getLogger() + 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/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 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 968929f..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,6 +26,8 @@ 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") @@ -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 = 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)) @@ -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"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,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) @@ -193,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: """ @@ -202,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) @@ -213,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..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 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 240edef..5faccb3 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -193,8 +193,15 @@ class UploadController: pass # get the hop addition times - c.execute('SELECT Zeit, Name 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) # get the misc addition times c.execute('SELECT Zugabedauer, Name FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) @@ -314,8 +321,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: @@ -981,13 +991,13 @@ class UploadController: 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", @@ -1000,7 +1010,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..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): 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/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):