Merge branch 'development' into hop_text

This commit is contained in:
Alexander Vollkopf 2022-10-03 11:37:19 +02:00 committed by GitHub
commit cb60b6d5c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 253 additions and 103 deletions

View file

@ -9,9 +9,10 @@ RUN apt-get install --no-install-recommends -y \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel
# Install craftbeerpi requirements for better caching # Install craftbeerpi requirements and additional-dev-requirements for better caching
COPY ./requirements.txt /workspace/requirements.txt COPY ./requirements.txt ./.devcontainer/cbpi-default-dev-config/additional-dev-requirements.txt /workspace/
RUN pip3 install --no-cache-dir -r /workspace/requirements.txt 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 # Install current version of cbpi-ui
RUN mkdir /opt/downloads \ RUN mkdir /opt/downloads \

View file

@ -0,0 +1 @@
cbpi4-SimulatedSensor==0.0.2

View file

@ -1,3 +0,0 @@
{
"elements": []
}

3
.gitignore vendored
View file

@ -18,3 +18,6 @@ node_modules
config/* config/*
logs/ logs/
.coverage .coverage
.devcontainer/cbpi-dev-config/*
cbpi4-*
temp*

29
.vscode/launch.json vendored
View file

@ -6,11 +6,36 @@
"configurations": [ "configurations": [
{ {
"name": "Run CraftBeerPi4", "name": "run CraftBeerPi4",
"type": "python", "type": "python",
"request": "launch", "request": "launch",
"module": "run", "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"]
} }
] ]
} }

20
.vscode/tasks.json vendored Normal file
View file

@ -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"
}
}
]
}

View file

@ -1,4 +1,5 @@
import logging import logging
from pathlib import Path
import requests import requests
from cbpi.configFolder import ConfigFolder from cbpi.configFolder import ConfigFolder
from cbpi.utils.utils import load_config from cbpi.utils.utils import load_config
@ -8,6 +9,7 @@ import os
import pkgutil import pkgutil
import shutil import shutil
import click import click
import pathlib
from subprocess import call from subprocess import call
from colorama import Fore, Back, Style from colorama import Fore, Back, Style
import importlib import importlib
@ -15,6 +17,7 @@ from importlib_metadata import metadata
from tabulate import tabulate from tabulate import tabulate
from PyInquirer import prompt, print_json from PyInquirer import prompt, print_json
import platform import platform
import time
class CraftBeerPiCli(): class CraftBeerPiCli():
def __init__(self, config) -> None: def __init__(self, config) -> None:
@ -85,7 +88,7 @@ class CraftBeerPiCli():
answers = prompt(questions) answers = prompt(questions)
name = "cbpi4_" + answers["name"] name = "cbpi4-" + str(answers["name"]).replace('_', '-').replace(' ', '-')
if os.path.exists(os.path.join(".", name)) is True: if os.path.exists(os.path.join(".", name)) is True:
print("Cant create Plugin. Folder {} already exists ".format(name)) print("Cant create Plugin. Folder {} already exists ".format(name))
return return
@ -98,7 +101,9 @@ class CraftBeerPiCli():
with ZipFile('temp.zip', 'r') as repo_zip: with ZipFile('temp.zip', 'r') as repo_zip:
repo_zip.extractall() repo_zip.extractall()
os.rename("./craftbeerpi4-plugin-template-main", os.path.join(".", name)) time.sleep(1) # windows dev container permissions problem otherwise
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)) os.rename(os.path.join(".", name, "src"), os.path.join(".", name, name))
import jinja2 import jinja2
@ -225,15 +230,28 @@ class CraftBeerPiCli():
@click.group() @click.group()
@click.pass_context @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'.") @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("---------------------")
print("Welcome to CBPi") print("Welcome to CBPi")
print("---------------------") print("---------------------")
level = logging.INFO if logs_folder_path == "":
logging.basicConfig(level=level, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s') logs_folder_path = os.path.join(Path(config_folder_path).absolute().parent, 'logs')
cbpi_cli = CraftBeerPiCli(ConfigFolder(config_folder_path)) 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 context.obj = cbpi_cli
pass
@main.command() @main.command()
@click.pass_context @click.pass_context

View file

@ -1,3 +0,0 @@
{
"elements": []
}

View file

@ -1,3 +1,4 @@
from ast import If, Try
import os import os
from os import listdir from os import listdir
from os.path import isfile, join from os.path import isfile, join
@ -5,53 +6,43 @@ import pathlib
import platform import platform
import shutil import shutil
import zipfile import zipfile
from pathlib import Path
import glob import glob
class ConfigFolder: class ConfigFolder:
def __init__(self, configFolderPath): def __init__(self, configFolderPath, logsFolderPath):
self._rawPath = configFolderPath self.configFolderPath = configFolderPath
self.logsFolderPath = logsFolderPath
print("config folder path : " + configFolderPath)
print("logs folder path : " + logsFolderPath)
def config_file_exists(self, path): def config_file_exists(self, path):
return os.path.exists(self.get_file_path(path)) return os.path.exists(self.get_file_path(path))
def get_file_path(self, file): 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): 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): 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): 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): 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")] 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 return fermenter_recipe_ids
def check_for_setup(self): def check_for_setup(self):
if self.config_file_exists("config.yaml") is False: # is there a restored_config.zip file? if yes restore it first then delte the zip.
print("***************************************************") backupfile = os.path.join(self.configFolderPath, "restored_config.zip")
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")
if os.path.exists(os.path.join(backupfile)) is True: if os.path.exists(os.path.join(backupfile)) is True:
print("***************************************************") print("***************************************************")
print("Found backup of config. Starting restore") print("Found backup of config. Starting restore")
@ -68,7 +59,7 @@ class ConfigFolder:
if zip_content == True: if zip_content == True:
print("Found correct content. Starting Restore process") print("Found correct content. Starting Restore process")
output_path = pathlib.Path(self._rawPath) output_path = pathlib.Path(self.configFolderPath)
system = platform.system() system = platform.system()
print(system) print(system)
if system != "Windows": if system != "Windows":
@ -83,17 +74,89 @@ class ConfigFolder:
print(f"Changing owner and group of config folder recursively to {owner}:{group}") print(f"Changing owner and group of config folder recursively to {owner}:{group}")
self.recursive_chown(output_path, owner, group) self.recursive_chown(output_path, owner, group)
print("Removing backup file") 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: else:
print("Wrong Content in zip file. No restore possible") print("Wrong Content in zip file. No restore possible")
print("Removing zip file") print("renaming zip file so it will be ignored on the next start")
os.remove(backupfile) 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("***************************************************") 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): def copyDefaultFileIfNotExists(self, file):
if self.config_file_exists(file) is False: if self.config_file_exists(file) is False:
srcfile = os.path.join(os.path.dirname(__file__), "config", file) 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) shutil.copy(srcfile, destfile)
def create_config_file(self): def create_config_file(self):
@ -107,27 +170,27 @@ class ConfigFolder:
self.copyDefaultFileIfNotExists("craftbeerpi.service") self.copyDefaultFileIfNotExists("craftbeerpi.service")
self.copyDefaultFileIfNotExists("chromium.desktop") 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") print("Config Folder created")
def create_home_folder_structure(configFolder): def create_home_folder_structure(configFolder):
pathlib.Path(os.path.join(".", 'logs/sensors')).mkdir(parents=True, exist_ok=True)
configFolder.create_folders() configFolder.create_folders()
print("Folder created") print("Folder created")
def create_folders(self): def create_folders(self):
pathlib.Path(self._rawPath).mkdir(parents=True, exist_ok=True) pathlib.Path(self.configFolderPath).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(self._rawPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True) pathlib.Path(self.logsFolderPath).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.configFolderPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True)
pathlib.Path(os.path.join(self._rawPath, 'upload')).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): def recursive_chown(self, path, owner, group):
for dirpath, dirnames, filenames in os.walk(path): try:
shutil.chown(dirpath, owner, group) for dirpath, dirnames, filenames in os.walk(path):
for filename in filenames: shutil.chown(dirpath, owner, group)
shutil.chown(os.path.join(dirpath, filename), 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")

View file

@ -1,6 +1,7 @@
from cbpi.api.dataclasses import Config from cbpi.api.dataclasses import Config
import logging import logging
import os import os
from pathlib import Path
from cbpi.api.config import ConfigType from cbpi.api.config import ConfigType
from cbpi.utils import load_config from cbpi.utils import load_config
@ -16,6 +17,7 @@ class ConfigController:
self.cbpi.register(self) self.cbpi.register(self)
self.path = cbpi.config_folder.get_file_path("config.json") self.path = cbpi.config_folder.get_file_path("config.json")
self.path_static = cbpi.config_folder.get_file_path("config.yaml") 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): def get_state(self):

View file

@ -18,14 +18,14 @@ class DashboardController:
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.cbpi.register(self) 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): async def init(self):
pass pass
async def get_content(self, dashboard_id): async def get_content(self, dashboard_id):
try: 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) logging.info(self.path)
with open(self.path) as json_file: with open(self.path) as json_file:
data = json.load(json_file) data = json.load(json_file)
@ -35,21 +35,21 @@ class DashboardController:
async def add_content(self, dashboard_id, data): async def add_content(self, dashboard_id, data):
print(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: with open(self.path, 'w') as outfile:
json.dump(data, outfile, indent=4, sort_keys=True) json.dump(data, outfile, indent=4, sort_keys=True)
self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Saved Successfully", type=NotificationType.SUCCESS) self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Saved Successfully", type=NotificationType.SUCCESS)
return {"status": "OK"} return {"status": "OK"}
async def delete_content(self, dashboard_id): 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): if os.path.exists(self.path):
os.remove(self.path) os.remove(self.path)
self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Deleted Successfully", type=NotificationType.SUCCESS) self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Deleted Successfully", type=NotificationType.SUCCESS)
async def get_custom_widgets(self): 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")] onlyfiles = [os.path.splitext(f)[0] for f in sorted(listdir(path)) if isfile(join(path, f)) and f.endswith(".svg")]
return onlyfiles return onlyfiles

View file

@ -8,6 +8,7 @@ import pandas as pd
import zipfile import zipfile
import base64 import base64
import urllib3 import urllib3
from pathlib import Path
from cbpi.api import * from cbpi.api import *
from cbpi.api.config import ConfigType from cbpi.api.config import ConfigType
from cbpi.api.base import CBPiBase from cbpi.api.base import CBPiBase
@ -25,6 +26,8 @@ class LogController:
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.configuration = False self.configuration = False
self.datalogger = {} 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: def log_data(self, name: str, value: str) -> None:
self.logfiles = self.cbpi.config.get("CSVLOGFILES", "Yes") self.logfiles = self.cbpi.config.get("CSVLOGFILES", "Yes")
@ -37,7 +40,7 @@ class LogController:
data_logger = logging.getLogger('cbpi.sensor.%s' % name) data_logger = logging.getLogger('cbpi.sensor.%s' % name)
data_logger.propagate = False data_logger.propagate = False
data_logger.setLevel(logging.DEBUG) 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) data_logger.addHandler(handler)
self.datalogger[name] = data_logger self.datalogger[name] = data_logger
@ -115,7 +118,7 @@ class LogController:
for name in names: for name in names:
# get all log 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 # 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]) 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)) logging.info("Read all files for {}".format(names))
@ -157,7 +160,7 @@ class LogController:
for id in ids: 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 # 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 = 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.resample('60s').max()
df = df.dropna() df = df.dropna()
@ -173,11 +176,10 @@ class LogController:
:return: list of log file names :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: def clear_log(self, name:str ) -> str:
all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*"))
all_filenames = glob.glob('./logs/sensor_%s.log*' % name)
for f in all_filenames: for f in all_filenames:
os.remove(f) os.remove(f)
@ -193,7 +195,7 @@ class LogController:
:return: :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: def clear_zip(self, name:str ) -> None:
""" """
@ -202,7 +204,7 @@ class LogController:
:return: None :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: for f in all_filenames:
os.remove(f) os.remove(f)
@ -213,9 +215,9 @@ class LogController:
""" """
formatted_time = strftime("%Y-%m-%d-%H_%M_%S", localtime()) 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) 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: for f in all_filenames:
zip.write(os.path.join(f)) zip.write(os.path.join(f))
zip.close() zip.close()

View file

@ -1,4 +1,5 @@
import asyncio import asyncio
from email import message
from cbpi.api.dataclasses import NotificationType from cbpi.api.dataclasses import NotificationType
import logging import logging
import shortuuid import shortuuid
@ -10,9 +11,23 @@ class NotificationController:
''' '''
self.cbpi = cbpi self.cbpi = cbpi
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
logging.root.addFilter(self.notify_log_event)
self.callback_cache = {} self.callback_cache = {}
self.listener = {} 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): def add_listener(self, method):
listener_id = shortuuid.uuid() listener_id = shortuuid.uuid()
self.listener[listener_id] = method self.listener[listener_id] = method

View file

@ -56,9 +56,9 @@ class SystemController:
output_filename="cbpi4_log.zip" output_filename="cbpi4_log.zip"
if logtime == "b": if logtime == "b":
os.system('journalctl -b -u craftbeerpi.service > {}'.format(fullname)) os.system('journalctl -b -u craftbeerpi.service --output cat > {}'.format(fullname))
else: 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)) os.system('cbpi plugins > {}'.format(fullpluginname))
@ -116,7 +116,7 @@ class SystemController:
try: try:
content = backup_file.read() content = backup_file.read()
if backup_file and self.allowed_file(filename, 'zip'): 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=open(self.path, "wb")
f.write(content) f.write(content)

View file

@ -193,8 +193,15 @@ class UploadController:
pass pass
# get the hop addition times # 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() 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 # get the misc addition times
c.execute('SELECT Zugabedauer, Name FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,)) c.execute('SELECT Zugabedauer, Name FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,))
@ -314,7 +321,10 @@ class UploadController:
await self.create_step(step_string) 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) self.cbpi.notify('KBH Recipe created', name, NotificationType.INFO)
@ -981,13 +991,13 @@ class UploadController:
return [alert, " and ".join(names)] return [alert, " and ".join(names)]
async def create_Whirlpool_Cooldown(self): async def create_Whirlpool_Cooldown(self, time : str = "15"):
# Add Waitstep as Whirlpool # Add Waitstep as Whirlpool
if self.cooldown != "WaiStep" and self.cooldown !="": if self.cooldown != "WaiStep" and self.cooldown !="":
step_string = { "name": "Whirlpool", step_string = { "name": "Whirlpool",
"props": { "props": {
"Kettle": self.boilid, "Kettle": self.boilid,
"Timer": "15" "Timer": time
}, },
"status_text": "", "status_text": "",
"status": "I", "status": "I",
@ -1000,7 +1010,7 @@ class UploadController:
step_name = "CoolDown" step_name = "CoolDown"
cooldown_sensor = "" cooldown_sensor = ""
step_temp = "" step_temp = ""
step_timer = "15" step_timer = time
if step_type == "CooldownStep": if step_type == "CooldownStep":
cooldown_sensor = self.cbpi.config.get("steps_cooldown_sensor", None) cooldown_sensor = self.cbpi.config.get("steps_cooldown_sensor", None)
if cooldown_sensor is None or cooldown_sensor == '': if cooldown_sensor is None or cooldown_sensor == '':

View file

@ -301,12 +301,6 @@ class CraftBeerPi:
self._swagger_setup() self._swagger_setup()
level = logging.INFO
logger = logging.getLogger()
logger.setLevel(level)
for handler in logger.handlers:
handler.setLevel(level)
return self.app return self.app
def start(self): def start(self):

View file

@ -2,6 +2,7 @@ from cbpi.utils.encoder import ComplexEncoder
from aiohttp import web from aiohttp import web
from cbpi.utils.utils import json_dumps from cbpi.utils.utils import json_dumps
from cbpi.api import request_mapping from cbpi.api import request_mapping
import os
import json import json
class LogHttpEndpoints: class LogHttpEndpoints:
@ -83,7 +84,7 @@ class LogHttpEndpoints:
) )
await response.prepare(request) await response.prepare(request)
log_name = request.match_info['name'] 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(): for line in file.readlines():
await response.write(line) await response.write(line)

View file

@ -44,7 +44,7 @@ class SystemHttpEndpoints:
async def http_get_log(self, request): async def http_get_log(self, request):
result = [] result = []
file_pattern = re.compile("^(\w+.).log(.?\d*)") 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): if file_pattern.match(filename):
result.append(filename) result.append(filename)
return web.json_response(result) return web.json_response(result)

View file

@ -19,5 +19,6 @@ class CraftBeerPiTestCase(AioHTTPTestCase):
def configuration(self): def configuration(self):
test_directory = os.path.dirname(__file__) test_directory = os.path.dirname(__file__)
test_config_directory = os.path.join(test_directory, 'cbpi-test-config') 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 return configFolder

View file

@ -9,7 +9,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
class CLITest(unittest.TestCase): class CLITest(unittest.TestCase):
def test_list(self): def test_list(self):
cli = CraftBeerPiCli(ConfigFolder("./cbpi-test-config")) cli = CraftBeerPiCli(ConfigFolder("./cbpi-test-config", './logs')) # inside tests folder
cli.plugins_list() cli.plugins_list()
if __name__ == '__main__': if __name__ == '__main__':

View file

@ -10,11 +10,11 @@ class LoggerTestCase(CraftBeerPiTestCase):
@unittest_run_loop @unittest_run_loop
async def test_log_data(self): 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" log_name = "test"
#clear all logs #clear all logs
self.cbpi.log.clear_log(log_name) 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 # write log entries
for i in range(5): for i in range(5):