Merge pull request #77 from avollkopf/development

Development
This commit is contained in:
Alexander Vollkopf 2022-12-10 15:14:33 +01:00 committed by GitHub
commit 08f292209f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 507 additions and 178 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

@ -177,6 +177,22 @@
"type": "select", "type": "select",
"value": 0 "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": { "RECIPE_CREATION_PATH": {
"description": "API path to creation plugin. Default: upload . CHANGE ONLY IF USING A RECIPE CREATION PLUGIN", "description": "API path to creation plugin. Default: upload . CHANGE ONLY IF USING A RECIPE CREATION PLUGIN",
"name": "RECIPE_CREATION_PATH", "name": "RECIPE_CREATION_PATH",

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,3 +1,3 @@
__version__ = "4.0.6" __version__ = "4.0.7"
__codename__ = "Spring Break" __codename__ = "November Rain"

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,29 @@ 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, 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 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

@ -66,3 +66,11 @@ class ActorController(BasicController):
self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict()) self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict())
except Exception as e: except Exception as e:
logging.error("Failed to update Actor {} {}".format(id, 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))

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
@ -64,3 +64,7 @@ class DashboardController:
async def set_current_dashboard(self, dashboard_id=1): async def set_current_dashboard(self, dashboard_id=1):
await self.cbpi.config.set("current_dashboard_number", dashboard_id) await self.cbpi.config.set("current_dashboard_number", dashboard_id)
return {"status": "OK"} 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

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,24 +26,26 @@ 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")
self.influxdb = self.cbpi.config.get("INFLUXDB", "No") self.influxdb = self.cbpi.config.get("INFLUXDB", "No")
if self.logfiles == "Yes": if self.logfiles == "Yes":
if name not in self.datalogger: if name not in self.datalogger:
max_bytes = self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 1048576) max_bytes = int(self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 100000))
backup_count = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3) backup_count = int(self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3))
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
formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime()) 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": if self.influxdb == "Yes":
self.influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", "No") self.influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", "No")
self.influxdbaddr = self.cbpi.config.get("INFLUXDBADDR", None) self.influxdbaddr = self.cbpi.config.get("INFLUXDBADDR", None)
@ -115,8 +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))
@ -125,6 +127,14 @@ class LogController:
df = df[name].resample(sample_rate).max() df = df[name].resample(sample_rate).max()
logging.info("Sampled now for {}".format(names)) logging.info("Sampled now for {}".format(names))
df = df.dropna() 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: if result is None:
result = df result = df
else: else:
@ -137,7 +147,9 @@ class LogController:
data[name] = result[name].interpolate(limit_direction='both', limit=10).tolist() data[name] = result[name].interpolate(limit_direction='both', limit=10).tolist()
else: else:
data[name] = result.interpolate().tolist() data[name] = result.interpolate().tolist()
logging.info("Send Log for {}".format(names)) logging.info("Send Log for {}".format(names))
return data return data
async def get_data2(self, ids) -> dict: async def get_data2(self, ids) -> dict:
@ -146,7 +158,12 @@ class LogController:
result = dict() result = dict()
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
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()} result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()}
return result return result
@ -159,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)
@ -179,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:
""" """
@ -188,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)
@ -199,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,5 +1,7 @@
import asyncio import asyncio
from email import message
from cbpi.api.dataclasses import NotificationType from cbpi.api.dataclasses import NotificationType
from cbpi.api import *
import logging import logging
import shortuuid import shortuuid
class NotificationController: class NotificationController:
@ -10,9 +12,26 @@ 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):
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): 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

@ -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", [])) 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): def get_sensor_value(self, id):
if id is None:
return None
try: try:
return self.find_by_id(id).instance.get_state() return self.find_by_id(id).instance.get_state()
except Exception as e: except Exception as e:

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,15 +193,21 @@ class UploadController:
pass pass
# get the hop addition times # 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() 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 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() miscs = c.fetchall()
try: 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() FW_Hops = c.fetchall()
FirstWort = self.getFirstWort(FW_Hops,"kbh") FirstWort = self.getFirstWort(FW_Hops,"kbh")
except: except:
@ -291,14 +297,21 @@ class UploadController:
"Sensor": self.boilkettle.sensor, "Sensor": self.boilkettle.sensor,
"Temp": int(self.BoilTemp), "Temp": int(self.BoilTemp),
"Timer": BoilTime, "Timer": BoilTime,
"First_Wort": FirstWort, "First_Wort": FirstWort[0],
"First_Wort_text": FirstWort[1],
"LidAlert": "Yes", "LidAlert": "Yes",
"Hop_1": Hops[0], "Hop_1": Hops[0][0],
"Hop_2": Hops[1], "Hop_1_text": Hops[0][1],
"Hop_3": Hops[2], "Hop_2": Hops[1][0],
"Hop_4": Hops[3], "Hop_2_text": Hops[1][1],
"Hop_5": Hops[4], "Hop_3": Hops[2][0],
"Hop_6": Hops[5] "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_text": "",
"status": "I", "status": "I",
@ -307,7 +320,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)
@ -354,11 +370,12 @@ class UploadController:
if e["Hopfen_{}_Kochzeit".format(idx)].isnumeric(): if e["Hopfen_{}_Kochzeit".format(idx)].isnumeric():
if boil_time is not e["Hopfen_{}_Kochzeit".format(idx)].isnumeric(): if boil_time is not e["Hopfen_{}_Kochzeit".format(idx)].isnumeric():
alert = float(e["Hopfen_{}_Kochzeit".format(idx)]) alert = float(e["Hopfen_{}_Kochzeit".format(idx)])
elif e["Hopfen_{}_Kochzeit".format(idx)] == "Whirlpool": elif e["Hopfen_{}_Kochzeit".format(idx)] == "Whirlpool" or float(e["Hopfen_{}_Kochzeit".format(idx)]) < 0:
alert = float(1) alert = float(0)
hops_name = hops_name + ' whirlpool'
else: else:
self.api.notify(headline="No Number at Hoptime", message="Please change json-File at Hopfen_{}_Kochzeit".format(idx), type="danger") self.cbpi.notify("No Number at Hoptime", "Please change json-File at Hopfen_{}_Kochzeit".format(idx), NotificationType.ERROR)
alert = float(1) alert = float(0)
hops.append({"name":hops_name,"time":alert}) 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)]) 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(): if e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)].isnumeric():
alert = float(e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)]) alert = float(e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)])
elif e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)] == "Whirlpool": elif e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)] == "Whirlpool" or float(e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)]) < 0:
alert = float(1) alert = float(0)
miscs_name = miscs_name + ' whirlpool'
else: else:
self.api.notify(headline="No Number at Hoptime", message="Please change json-File at WeitereZutat_Wuerze_{}_Kochzeit".format(idx), type="danger") 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}) miscs.append({"name":miscs_name,"time":alert})
@ -510,14 +528,21 @@ class UploadController:
"Sensor": sensor, "Sensor": sensor,
"Temp": step_temp, "Temp": step_temp,
"Timer": step_time, "Timer": step_time,
"First_Wort": FirstWort, "First_Wort": FirstWort[0],
"First_Wort_text": FirstWort[1],
"LidAlert": LidAlert, "LidAlert": LidAlert,
"Hop_1": Hops[0], "Hop_1": Hops[0][0],
"Hop_2": Hops[1], "Hop_1_text": Hops[0][1],
"Hop_3": Hops[2], "Hop_2": Hops[1][0],
"Hop_4": Hops[3], "Hop_2_text": Hops[1][1],
"Hop_5": Hops[4], "Hop_3": Hops[2][0],
"Hop_6": Hops[5] "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_text": "",
"status": "I", "status": "I",
@ -656,14 +681,21 @@ class UploadController:
"Sensor": sensor, "Sensor": sensor,
"Temp": step_temp, "Temp": step_temp,
"Timer": step_time, "Timer": step_time,
"First_Wort": FirstWort, "First_Wort": FirstWort[0],
"First_Wort_text": FirstWort[1],
"LidAlert": LidAlert, "LidAlert": LidAlert,
"Hop_1": Hops[0], "Hop_1": Hops[0][0],
"Hop_2": Hops[1], "Hop_1_text": Hops[0][1],
"Hop_3": Hops[2], "Hop_2": Hops[1][0],
"Hop_4": Hops[3], "Hop_2_text": Hops[1][1],
"Hop_5": Hops[4], "Hop_3": Hops[2][0],
"Hop_6": Hops[5] "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_text": "",
"status": "I", "status": "I",
@ -858,14 +890,21 @@ class UploadController:
"Sensor": sensor, "Sensor": sensor,
"Temp": step_temp, "Temp": step_temp,
"Timer": step_time, "Timer": step_time,
"First_Wort": FirstWort, "First_Wort": FirstWort[0],
"First_Wort_text": FirstWort[1],
"LidAlert": LidAlert, "LidAlert": LidAlert,
"Hop_1": Hops[0], "Hop_1": Hops[0][0],
"Hop_2": Hops[1], "Hop_1_text": Hops[0][1],
"Hop_3": Hops[2], "Hop_2": Hops[1][0],
"Hop_4": Hops[3], "Hop_2_text": Hops[1][1],
"Hop_5": Hops[4], "Hop_3": Hops[2][0],
"Hop_6": Hops[5] "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_text": "",
"status": "I", "status": "I",
@ -888,48 +927,51 @@ class UploadController:
## Hops which are not used in the boil step should not cause alerts ## Hops which are not used in the boil step should not cause alerts
if use != 'Aroma' and use != 'Boil': if use != 'Aroma' and use != 'Boil':
continue continue
alerts.append(float(hop.find('TIME').text)) alerts.append([float(hop.find('TIME').text), hop.find('NAME').text])
elif recipe_type == "bf": elif recipe_type == "bf":
use = hop['use'] use = hop['use']
if use != 'Aroma' and use != 'Boil': if use != 'Aroma' and use != 'Boil':
continue continue
alerts.append(float(hop['time'])) alerts.append([float(hop['time']), hop['name']]) ## TODO: Testing
elif recipe_type == "kbh": elif recipe_type == "kbh":
alerts.append(float(hop[0])) alerts.append([float(hop[0]), hop[1]])
elif recipe_type == "json": 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 ## There might also be miscelaneous additions during boild time
if miscs is not None: if miscs is not None:
for misc in miscs: for misc in miscs:
if recipe_type == "xml": 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": elif recipe_type == "bf":
use = misc['use'] use = misc['use']
if use != 'Aroma' and use != 'Boil': if use != 'Aroma' and use != 'Boil':
continue continue
alerts.append(float(misc['time'])) alerts.append([float(misc['time']), misc['name']]) ## TODO: Testing
elif recipe_type == "kbh": elif recipe_type == "kbh":
alerts.append(float(misc[0])) alerts.append([float(misc[0]), misc[1]])
elif recipe_type == "json": elif recipe_type == "json":
alerts.append(float(misc['time'])) alerts.append([float(misc['time']), misc['name']])
## Dedupe and order the additions by their time, to prevent multiple alerts at the same time ## Dedupe and order the additions by their time
alerts = sorted(list(set(alerts)))
## CBP should have these additions in reverse ## CBP should have these additions in reverse
alerts.reverse() alerts = sorted(alerts, key=lambda x:x[0], reverse=True)
hop_alerts = [] hop_alerts = [[None, None],[None, None],[None, None],[None, None],[None, None],[None, None]]
for i in range(0,6): for i in range(0,6):
try: try:
hop_alerts.append(str(int(alerts[i]))) if float(alerts[i][0]) > -1:
hop_alerts[i] = alerts[i]
except: except:
hop_alerts.append(None) pass
return hop_alerts return hop_alerts
def getFirstWort(self, hops, recipe_type): def getFirstWort(self, hops, recipe_type):
alert = "No" alert = "No"
names = []
if recipe_type == "kbh": if recipe_type == "kbh":
if len(hops) != 0: if len(hops) != 0:
alert = "Yes" alert = "Yes"
for hop in hops:
names.append(hop[1])
elif recipe_type == "xml": elif recipe_type == "xml":
for hop in hops: for hop in hops:
use = hop.find('USE').text use = hop.find('USE').text
@ -937,22 +979,27 @@ class UploadController:
if use != 'First Wort': if use != 'First Wort':
continue continue
alert = "Yes" alert = "Yes"
names.append(hop.find('NAME').text)
elif recipe_type == "bf": elif recipe_type == "bf":
for hop in hops: for hop in hops:
if hop['use'] == "First Wort": if hop['use'] == "First Wort":
alert="Yes" alert="Yes"
names.append(hop['name']) ## TODO: Testing
elif recipe_type == "json": elif recipe_type == "json":
for hop in hops: if len(hops) != 0:
alert="Yes" alert = "Yes"
return alert for hop in hops:
names.append(hop['name'])
async def create_Whirlpool_Cooldown(self): return [alert, " and ".join(names)]
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",
@ -965,7 +1012,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

@ -47,7 +47,10 @@ class ConfigUpdate(CBPiExtension):
influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None) influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None)
mqttupdate = self.cbpi.config.get("MQTTUpdate", None) mqttupdate = self.cbpi.config.get("MQTTUpdate", None)
PRESSURE_UNIT = self.cbpi.config.get("PRESSURE_UNIT", 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: if boil_temp is None:
logger.info("INIT Boil Temp Setting") logger.info("INIT Boil Temp Setting")
@ -286,6 +289,42 @@ class ConfigUpdate(CBPiExtension):
except: except:
logger.warning('Unable to update config') 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): def setup(cbpi):
cbpi.plugin.register("ConfigUpdate", ConfigUpdate) cbpi.plugin.register("ConfigUpdate", ConfigUpdate)
pass pass

View file

@ -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="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(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.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.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.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.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.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_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): class BoilStep(CBPiStep):
@action("Start Timer", []) @action("Start Timer", [])
@ -367,6 +374,7 @@ class BoilStep(CBPiStep):
self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False
self.first_wort_hop_flag = False self.first_wort_hop_flag = False
self.first_wort_hop=self.props.get("First_Wort", "No") 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.hops_added=["","","","","",""]
self.remaining_seconds = None self.remaining_seconds = None
@ -389,11 +397,14 @@ class BoilStep(CBPiStep):
await self.setAutoMode(True) await self.setAutoMode(True)
await self.push_update() 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 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): if self.remaining_seconds != None and self.remaining_seconds <= (int(value) * 60 + 1):
self.hops_added[number-1]= True 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): async def on_stop(self):
await self.timer.stop() await self.timer.stop()
@ -409,7 +420,10 @@ class BoilStep(CBPiStep):
async def run(self): async def run(self):
if self.first_wort_hop_flag == False and self.first_wort_hop == "Yes": if self.first_wort_hop_flag == False and self.first_wort_hop == "Yes":
self.first_wort_hop_flag = True 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: while self.running == True:
await asyncio.sleep(1) 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) self.cbpi.notify(self.name, 'Timer started. Estimated completion: {}'.format(estimated_completion_time.strftime("%H:%M")), NotificationType.INFO)
else: else:
for x in range(1, 6): 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 return StepResult.DONE

View file

@ -32,6 +32,6 @@ class GenericMqttActor(MQTTActor):
self.state = True self.state = True
async def off(self): 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) await self.publish_mqtt_message(self.topic, formatted_payload)
self.state = False self.state = False

View file

@ -39,7 +39,7 @@ class MQTTActor(CBPiActor):
async def off(self): async def off(self):
self.state = False self.state = False
await self.cbpi.satellite.publish(self.topic, json.dumps( await self.cbpi.satellite.publish(self.topic, json.dumps(
{"state": "off", "power": self.power}), True) {"state": "off", "power": 0}), True)
pass pass
async def run(self): async def run(self):

View file

@ -24,6 +24,21 @@ class ActorHttpEndpoints():
""" """
return web.json_response(data=self.controller.get_state()) 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) @request_mapping(path="/{id:\w+}", auth_required=False)
async def http_get_one(self, request): async def http_get_one(self, request):
""" """

View file

@ -157,3 +157,15 @@ class DashBoardHttpEndpoints:
dashboard_id = int(request.match_info['id']) dashboard_id = int(request.match_info['id'])
return web.json_response(await self.cbpi.dashboard.set_current_dashboard(dashboard_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)

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

@ -21,9 +21,9 @@ class Scheduler(*bases):
self._close_timeout = close_timeout self._close_timeout = close_timeout
self._limit = limit self._limit = limit
self._exception_handler = exception_handler 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._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 self._closed = False
def __iter__(self): def __iter__(self):

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):

13
testversion.py Normal file
View file

@ -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')