mirror of
https://github.com/PiBrewing/craftbeerpi4.git
synced 2024-12-26 15:34:55 +01:00
commit
08f292209f
45 changed files with 507 additions and 178 deletions
|
@ -9,9 +9,10 @@ RUN apt-get install --no-install-recommends -y \
|
|||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN python3 -m pip install --no-cache-dir --upgrade pip setuptools wheel
|
||||
|
||||
# Install craftbeerpi requirements for better caching
|
||||
COPY ./requirements.txt /workspace/requirements.txt
|
||||
RUN pip3 install --no-cache-dir -r /workspace/requirements.txt
|
||||
# Install craftbeerpi requirements and additional-dev-requirements for better caching
|
||||
COPY ./requirements.txt ./.devcontainer/cbpi-default-dev-config/additional-dev-requirements.txt /workspace/
|
||||
RUN cat /workspace/additional-dev-requirements.txt 2>/dev/null 1>> /workspace/requirements.txt \
|
||||
&& pip3 install --no-cache-dir -r /workspace/requirements.txt
|
||||
|
||||
# Install current version of cbpi-ui
|
||||
RUN mkdir /opt/downloads \
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
cbpi4-SimulatedSensor==0.0.2
|
|
@ -177,6 +177,22 @@
|
|||
"type": "select",
|
||||
"value": 0
|
||||
},
|
||||
"PRESSURE_UNIT": {
|
||||
"description": "Set unit for pressure",
|
||||
"name": "PRESSURE_UNIT",
|
||||
"options": [
|
||||
{
|
||||
"label": "kPa",
|
||||
"value": "kPa"
|
||||
},
|
||||
{
|
||||
"label": "PSI",
|
||||
"value": "PSI"
|
||||
}
|
||||
],
|
||||
"type": "select",
|
||||
"value": "kPa"
|
||||
},
|
||||
"RECIPE_CREATION_PATH": {
|
||||
"description": "API path to creation plugin. Default: upload . CHANGE ONLY IF USING A RECIPE CREATION PLUGIN",
|
||||
"name": "RECIPE_CREATION_PATH",
|
0
.devcontainer/cbpi-default-dev-config/recipes/.gitkeep
Normal file
0
.devcontainer/cbpi-default-dev-config/recipes/.gitkeep
Normal file
0
.devcontainer/cbpi-default-dev-config/upload/.gitkeep
Normal file
0
.devcontainer/cbpi-default-dev-config/upload/.gitkeep
Normal file
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"elements": []
|
||||
}
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -17,4 +17,7 @@ node_modules
|
|||
.DS_Store
|
||||
config/*
|
||||
logs/
|
||||
.coverage
|
||||
.coverage
|
||||
.devcontainer/cbpi-dev-config/*
|
||||
cbpi4-*
|
||||
temp*
|
29
.vscode/launch.json
vendored
29
.vscode/launch.json
vendored
|
@ -6,11 +6,36 @@
|
|||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "Run CraftBeerPi4",
|
||||
"name": "run CraftBeerPi4",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "run",
|
||||
"args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "start"]
|
||||
"args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "start"],
|
||||
"preLaunchTask": "copy default cbpi config files if dev config files dont exist"
|
||||
},
|
||||
|
||||
{
|
||||
"name": "create CraftBeerPi4 plugin",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "run",
|
||||
"args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "create"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "setup CraftBeerPi4: create config folder structure",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "run",
|
||||
"args": ["--config-folder-path=./.devcontainer/cbpi-dev-config", "setup"]
|
||||
},
|
||||
|
||||
{
|
||||
"name": "run tests",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"module": "pytest",
|
||||
"args": ["tests"]
|
||||
}
|
||||
]
|
||||
}
|
20
.vscode/tasks.json
vendored
Normal file
20
.vscode/tasks.json
vendored
Normal 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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
__version__ = "4.0.6"
|
||||
__codename__ = "Spring Break"
|
||||
__version__ = "4.0.7"
|
||||
__codename__ = "November Rain"
|
||||
|
||||
|
|
33
cbpi/cli.py
33
cbpi/cli.py
|
@ -1,4 +1,5 @@
|
|||
import logging
|
||||
from pathlib import Path
|
||||
import requests
|
||||
from cbpi.configFolder import ConfigFolder
|
||||
from cbpi.utils.utils import load_config
|
||||
|
@ -8,6 +9,7 @@ import os
|
|||
import pkgutil
|
||||
import shutil
|
||||
import click
|
||||
import pathlib
|
||||
from subprocess import call
|
||||
from colorama import Fore, Back, Style
|
||||
import importlib
|
||||
|
@ -15,6 +17,7 @@ from importlib_metadata import metadata
|
|||
from tabulate import tabulate
|
||||
from PyInquirer import prompt, print_json
|
||||
import platform
|
||||
import time
|
||||
|
||||
class CraftBeerPiCli():
|
||||
def __init__(self, config) -> None:
|
||||
|
@ -85,7 +88,7 @@ class CraftBeerPiCli():
|
|||
|
||||
answers = prompt(questions)
|
||||
|
||||
name = "cbpi4_" + answers["name"]
|
||||
name = "cbpi4-" + str(answers["name"]).replace('_', '-').replace(' ', '-')
|
||||
if os.path.exists(os.path.join(".", name)) is True:
|
||||
print("Cant create Plugin. Folder {} already exists ".format(name))
|
||||
return
|
||||
|
@ -97,8 +100,10 @@ class CraftBeerPiCli():
|
|||
|
||||
with ZipFile('temp.zip', 'r') as repo_zip:
|
||||
repo_zip.extractall()
|
||||
|
||||
time.sleep(1) # windows dev container permissions problem otherwise
|
||||
|
||||
os.rename("./craftbeerpi4-plugin-template-main", os.path.join(".", name))
|
||||
os.rename(os.path.join(".","craftbeerpi4-plugin-template-main"), os.path.join(".", name))
|
||||
os.rename(os.path.join(".", name, "src"), os.path.join(".", name, name))
|
||||
|
||||
import jinja2
|
||||
|
@ -225,15 +230,29 @@ class CraftBeerPiCli():
|
|||
@click.group()
|
||||
@click.pass_context
|
||||
@click.option('--config-folder-path', '-c', default="./config", type=click.Path(), help="Specify where the config folder is located. Defaults to './config'.")
|
||||
def main(context, config_folder_path):
|
||||
@click.option('--logs-folder-path', '-l', default="", type=click.Path(), help="Specify where the log folder is located. Defaults to '../logs' relative from the config folder.")
|
||||
@click.option('--debug-log-level', '-d', default="30", type=int, help="Specify the log level you want to write to all logs. 0=ALL, 10=DEBUG, 20=INFO 30(default)=WARNING, 40=ERROR, 50=CRITICAL")
|
||||
def main(context, config_folder_path, logs_folder_path, debug_log_level):
|
||||
print("---------------------")
|
||||
print("Welcome to CBPi")
|
||||
print("---------------------")
|
||||
level = logging.INFO
|
||||
logging.basicConfig(level=level, format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
|
||||
cbpi_cli = CraftBeerPiCli(ConfigFolder(config_folder_path))
|
||||
if logs_folder_path == "":
|
||||
logs_folder_path = os.path.join(Path(config_folder_path).absolute().parent, 'logs')
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(name)s - %(message)s')
|
||||
logging.basicConfig(format=formatter, stream=logging.StreamHandler())
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(debug_log_level)
|
||||
try:
|
||||
if not os.path.isdir(logs_folder_path):
|
||||
logger.info(f"logs folder '{logs_folder_path}' doesnt exist and we are trying to create it")
|
||||
pathlib.Path(logs_folder_path).mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"logs folder '{logs_folder_path}' successfully created")
|
||||
logger.addHandler(logging.handlers.RotatingFileHandler(os.path.join(logs_folder_path, f"cbpi.log"), maxBytes=1000000, backupCount=3))
|
||||
except Exception as e:
|
||||
logger.warning("log folder or log file could not be created or accessed. check folder and file permissions or create the logs folder somewhere you have access with a start option like '--log-folder-path=./logs'")
|
||||
logging.critical(e, exc_info=True)
|
||||
cbpi_cli = CraftBeerPiCli(ConfigFolder(config_folder_path, logs_folder_path))
|
||||
context.obj = cbpi_cli
|
||||
pass
|
||||
|
||||
@main.command()
|
||||
@click.pass_context
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"elements": []
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
from ast import If, Try
|
||||
import os
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
|
@ -5,53 +6,43 @@ import pathlib
|
|||
import platform
|
||||
import shutil
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
import glob
|
||||
|
||||
|
||||
class ConfigFolder:
|
||||
def __init__(self, configFolderPath):
|
||||
self._rawPath = configFolderPath
|
||||
def __init__(self, configFolderPath, logsFolderPath):
|
||||
self.configFolderPath = configFolderPath
|
||||
self.logsFolderPath = logsFolderPath
|
||||
print("config folder path : " + configFolderPath)
|
||||
print("logs folder path : " + logsFolderPath)
|
||||
|
||||
def config_file_exists(self, path):
|
||||
return os.path.exists(self.get_file_path(path))
|
||||
|
||||
def get_file_path(self, file):
|
||||
return os.path.join(self._rawPath, file)
|
||||
return os.path.join(self.configFolderPath, file)
|
||||
|
||||
def get_dashboard_path(self, file):
|
||||
return os.path.join(self.configFolderPath, "dashboard", file)
|
||||
|
||||
def get_upload_file(self, file):
|
||||
return os.path.join(self._rawPath, 'upload', file)
|
||||
return os.path.join(self.configFolderPath, 'upload', file)
|
||||
|
||||
def get_recipe_file_by_id(self, recipe_id):
|
||||
return os.path.join(self._rawPath, 'recipes', "{}.yaml".format(recipe_id))
|
||||
return os.path.join(self.configFolderPath, 'recipes', "{}.yaml".format(recipe_id))
|
||||
|
||||
def get_fermenter_recipe_by_id(self, recipe_id):
|
||||
return os.path.join(self._rawPath, 'fermenterrecipes', "{}.yaml".format(recipe_id))
|
||||
return os.path.join(self.configFolderPath, 'fermenterrecipes', "{}.yaml".format(recipe_id))
|
||||
|
||||
def get_all_fermenter_recipes(self):
|
||||
fermenter_recipes_folder = os.path.join(self._rawPath, 'fermenterrecipes')
|
||||
fermenter_recipes_folder = os.path.join(self.configFolderPath, 'fermenterrecipes')
|
||||
fermenter_recipe_ids = [os.path.splitext(f)[0] for f in listdir(fermenter_recipes_folder) if isfile(join(fermenter_recipes_folder, f)) and f.endswith(".yaml")]
|
||||
return fermenter_recipe_ids
|
||||
|
||||
def check_for_setup(self):
|
||||
if self.config_file_exists("config.yaml") is False:
|
||||
print("***************************************************")
|
||||
print("CraftBeerPi Config File not found: %s" % self.get_file_path("config.yaml"))
|
||||
print("Please run 'cbpi setup' before starting the server ")
|
||||
print("***************************************************")
|
||||
return False
|
||||
if self.config_file_exists("upload") is False:
|
||||
print("***************************************************")
|
||||
print("CraftBeerPi upload folder not found: %s" % self.get_file_path("upload"))
|
||||
print("Please run 'cbpi setup' before starting the server ")
|
||||
print("***************************************************")
|
||||
return False
|
||||
# if os.path.exists(os.path.join(".", "config", "fermenterrecipes")) is False:
|
||||
# print("***************************************************")
|
||||
# print("CraftBeerPi fermenterrecipes folder not found: %s" % os.path.join(".", "config/fermenterrecipes"))
|
||||
# print("Please run 'cbpi setup' before starting the server ")
|
||||
# print("***************************************************")
|
||||
# return False
|
||||
backupfile = os.path.join(".", "restored_config.zip")
|
||||
# is there a restored_config.zip file? if yes restore it first then delte the zip.
|
||||
backupfile = os.path.join(self.configFolderPath, "restored_config.zip")
|
||||
if os.path.exists(os.path.join(backupfile)) is True:
|
||||
print("***************************************************")
|
||||
print("Found backup of config. Starting restore")
|
||||
|
@ -68,14 +59,14 @@ class ConfigFolder:
|
|||
|
||||
if zip_content == True:
|
||||
print("Found correct content. Starting Restore process")
|
||||
output_path = pathlib.Path(self._rawPath)
|
||||
output_path = pathlib.Path(self.configFolderPath)
|
||||
system = platform.system()
|
||||
print(system)
|
||||
if system != "Windows":
|
||||
owner = output_path.owner()
|
||||
group = output_path.group()
|
||||
print("Removing old config folder")
|
||||
shutil.rmtree(output_path, ignore_errors=True)
|
||||
shutil.rmtree(output_path, ignore_errors=True)
|
||||
print("Extracting zip file to config folder")
|
||||
zip.extractall(output_path)
|
||||
zip.close()
|
||||
|
@ -83,17 +74,89 @@ class ConfigFolder:
|
|||
print(f"Changing owner and group of config folder recursively to {owner}:{group}")
|
||||
self.recursive_chown(output_path, owner, group)
|
||||
print("Removing backup file")
|
||||
os.remove(backupfile)
|
||||
print("contents of restored_config.zip file have been restored.")
|
||||
print("in case of a partial backup you will still be prompted to run 'cbpi setup'.")
|
||||
else:
|
||||
print("Wrong Content in zip file. No restore possible")
|
||||
print("Removing zip file")
|
||||
os.remove(backupfile)
|
||||
print("renaming zip file so it will be ignored on the next start")
|
||||
try:
|
||||
os.rename(backupfile, os.path.join(self.configFolderPath, "UNRESTORABLE_restored_config.zip"))
|
||||
except:
|
||||
print("renamed file does exist - deleting instead")
|
||||
os.remove(backupfile)
|
||||
print("***************************************************")
|
||||
# possible restored_config.zip has been handeled now lets check if files and folders exist
|
||||
required_config_content = [
|
||||
['config.yaml', 'file'],
|
||||
['actor.json', 'file'],
|
||||
['sensor.json', 'file'],
|
||||
['kettle.json', 'file'],
|
||||
['fermenter_data.json', 'file'],
|
||||
['step_data.json', 'file'],
|
||||
['config.json', 'file'],
|
||||
['craftbeerpi.service', 'file'],
|
||||
['chromium.desktop', 'file'],
|
||||
['dashboard', 'folder'],
|
||||
['dashboard/widgets', 'folder'],
|
||||
['fermenterrecipes', 'folder'],
|
||||
[self.logsFolderPath, 'folder'],
|
||||
['recipes', 'folder'],
|
||||
['upload', 'folder']
|
||||
#['dashboard/cbpi_dashboard_1.json', 'file'] no need to check - can be created with online editor
|
||||
]
|
||||
for checking in required_config_content:
|
||||
if self.inform_missing_content(self.check_for_file_or_folder(os.path.join(self.configFolderPath, checking[0]), checking[1])):
|
||||
# since there is no complete config we now check if the config folde rmay be completely empty to show hints:
|
||||
if len(os.listdir(os.path.join(self.configFolderPath))) == 0 :
|
||||
print("***************************************************")
|
||||
print(f"the config folder '{self.configFolderPath}' seems to be completely empty")
|
||||
print("you might want to run 'cbpi setup'.print")
|
||||
print("but you could also place your zipped config backup named")
|
||||
print("'restored_config.zip' inside the mentioned config folder for")
|
||||
print("cbpi4 to automatically unpack it")
|
||||
print("of course you can also place your config files manually")
|
||||
print("***************************************************")
|
||||
return False
|
||||
|
||||
# if cbpi_dashboard_1.json doesnt exist at the new location (configFolderPath/dashboard)
|
||||
# we move every cbpi_dashboard_n.json file from the old location (configFolderPath) there.
|
||||
# this could be a config zip file restore from version 4.0.7.a4 or prior.
|
||||
if not (os.path.isfile(os.path.join(self.configFolderPath, 'dashboard', 'cbpi_dashboard_1.json'))):
|
||||
for file in glob.glob(os.path.join(self.configFolderPath, 'cbpi_dashboard_*.json')):
|
||||
shutil.move(file, os.path.join(self.configFolderPath, 'dashboard', os.path.basename(file)))
|
||||
|
||||
def inform_missing_content(self, whatsmissing : str):
|
||||
if whatsmissing == "":
|
||||
return False
|
||||
print("***************************************************")
|
||||
print(f"CraftBeerPi config content not found: {whatsmissing}")
|
||||
print("Please run 'cbpi setup' before starting the server ")
|
||||
print("***************************************************")
|
||||
return True
|
||||
|
||||
|
||||
def check_for_file_or_folder(self, path : str, file_or_folder : str = ""): # file_or_folder should be "file" or "folder" or "" if both is ok
|
||||
if (file_or_folder == ""): # file and folder is ok
|
||||
if os.path.exists(path):
|
||||
return ""
|
||||
else:
|
||||
return "file or folder missing: " + path
|
||||
if (file_or_folder == "file"): # only file is ok
|
||||
if (os.path.isfile(path)):
|
||||
return ""
|
||||
else:
|
||||
return "file missing: " + path
|
||||
if (file_or_folder == "folder"): # oly folder is ok
|
||||
if (os.path.isdir(path)):
|
||||
return ""
|
||||
else:
|
||||
return "folder missing: " + path
|
||||
return "usage of check_file_or_folder() function wrong. second Argument must either be 'file' or 'folder' or an empty string"
|
||||
|
||||
def copyDefaultFileIfNotExists(self, file):
|
||||
if self.config_file_exists(file) is False:
|
||||
srcfile = os.path.join(os.path.dirname(__file__), "config", file)
|
||||
destfile = os.path.join(self._rawPath, file)
|
||||
destfile = os.path.join(self.configFolderPath, file)
|
||||
shutil.copy(srcfile, destfile)
|
||||
|
||||
def create_config_file(self):
|
||||
|
@ -107,27 +170,27 @@ class ConfigFolder:
|
|||
self.copyDefaultFileIfNotExists("craftbeerpi.service")
|
||||
self.copyDefaultFileIfNotExists("chromium.desktop")
|
||||
|
||||
if os.path.exists(os.path.join(self._rawPath, "dashboard", "cbpi_dashboard_1.json")) is False:
|
||||
srcfile = os.path.join(os.path.dirname(__file__), "config", "dashboard", "cbpi_dashboard_1.json")
|
||||
destfile = os.path.join(self._rawPath, "dashboard")
|
||||
shutil.copy(srcfile, destfile)
|
||||
|
||||
print("Config Folder created")
|
||||
|
||||
def create_home_folder_structure(configFolder):
|
||||
pathlib.Path(os.path.join(".", 'logs/sensors')).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
configFolder.create_folders()
|
||||
print("Folder created")
|
||||
|
||||
def create_folders(self):
|
||||
pathlib.Path(self._rawPath).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(self._rawPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(self._rawPath, 'recipes')).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(self._rawPath, 'upload')).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.configFolderPath).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.logsFolderPath).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(self.configFolderPath, 'dashboard', 'widgets')).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(self.configFolderPath, 'recipes')).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(self.configFolderPath, 'fermenterrecipes')).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(os.path.join(self.configFolderPath, 'upload')).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def recursive_chown(self, path, owner, group):
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
shutil.chown(dirpath, owner, group)
|
||||
for filename in filenames:
|
||||
shutil.chown(os.path.join(dirpath, filename), owner, group)
|
||||
try:
|
||||
for dirpath, dirnames, filenames in os.walk(path):
|
||||
shutil.chown(dirpath, owner, group)
|
||||
for filename in filenames:
|
||||
shutil.chown(os.path.join(dirpath, filename), owner, group)
|
||||
except:
|
||||
print("problems assigning file or folder permissions")
|
||||
print("if this happend on windows its fine")
|
||||
print("if this happend in the dev container running inside windows its also fine but you might have to rebuild the container if you run into further problems")
|
||||
|
|
|
@ -66,3 +66,11 @@ class ActorController(BasicController):
|
|||
self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict())
|
||||
except Exception as e:
|
||||
logging.error("Failed to update Actor {} {}".format(id, e))
|
||||
|
||||
async def ws_actor_update(self):
|
||||
try:
|
||||
#await self.push_udpate()
|
||||
self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda x: x.to_dict(), self.data))),self.sorting)
|
||||
# self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict())
|
||||
except Exception as e:
|
||||
logging.error("Failed to update Actors {}".format(e))
|
|
@ -1,6 +1,7 @@
|
|||
from cbpi.api.dataclasses import Config
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from cbpi.api.config import ConfigType
|
||||
from cbpi.utils import load_config
|
||||
|
@ -16,6 +17,7 @@ class ConfigController:
|
|||
self.cbpi.register(self)
|
||||
self.path = cbpi.config_folder.get_file_path("config.json")
|
||||
self.path_static = cbpi.config_folder.get_file_path("config.yaml")
|
||||
self.logger.info("Config folder path : " + os.path.join(Path(self.cbpi.config_folder.configFolderPath).absolute()))
|
||||
|
||||
def get_state(self):
|
||||
|
||||
|
|
|
@ -18,14 +18,14 @@ class DashboardController:
|
|||
self.logger = logging.getLogger(__name__)
|
||||
self.cbpi.register(self)
|
||||
|
||||
self.path = cbpi.config_folder.get_file_path("cbpi_dashboard_1.json")
|
||||
self.path = cbpi.config_folder.get_dashboard_path("cbpi_dashboard_1.json")
|
||||
|
||||
async def init(self):
|
||||
pass
|
||||
|
||||
async def get_content(self, dashboard_id):
|
||||
try:
|
||||
self.path = self.cbpi.config_folder.get_file_path("cbpi_dashboard_"+ str(dashboard_id) +".json")
|
||||
self.path = self.cbpi.config_folder.get_dashboard_path("cbpi_dashboard_"+ str(dashboard_id) +".json")
|
||||
logging.info(self.path)
|
||||
with open(self.path) as json_file:
|
||||
data = json.load(json_file)
|
||||
|
@ -35,21 +35,21 @@ class DashboardController:
|
|||
|
||||
async def add_content(self, dashboard_id, data):
|
||||
print(data)
|
||||
self.path = self.cbpi.config_folder.get_file_path("cbpi_dashboard_" + str(dashboard_id)+ ".json")
|
||||
self.path = self.cbpi.config_folder.get_dashboard_path("cbpi_dashboard_" + str(dashboard_id)+ ".json")
|
||||
with open(self.path, 'w') as outfile:
|
||||
json.dump(data, outfile, indent=4, sort_keys=True)
|
||||
self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Saved Successfully", type=NotificationType.SUCCESS)
|
||||
return {"status": "OK"}
|
||||
|
||||
async def delete_content(self, dashboard_id):
|
||||
self.path = self.cbpi.config_folder.get_file_path("cbpi_dashboard_"+ str(dashboard_id)+ ".json")
|
||||
self.path = self.cbpi.config_folder.get_dashboard_path("cbpi_dashboard_"+ str(dashboard_id)+ ".json")
|
||||
if os.path.exists(self.path):
|
||||
os.remove(self.path)
|
||||
self.cbpi.notify(title="Dashboard {}".format(dashboard_id), message="Deleted Successfully", type=NotificationType.SUCCESS)
|
||||
|
||||
|
||||
async def get_custom_widgets(self):
|
||||
path = os.path.join(self.cbpi.config_folder.get_file_path("dashboard"), "widgets")
|
||||
path = self.cbpi.config_folder.get_dashboard_path("widgets")
|
||||
onlyfiles = [os.path.splitext(f)[0] for f in sorted(listdir(path)) if isfile(join(path, f)) and f.endswith(".svg")]
|
||||
return onlyfiles
|
||||
|
||||
|
@ -64,3 +64,7 @@ class DashboardController:
|
|||
async def set_current_dashboard(self, dashboard_id=1):
|
||||
await self.cbpi.config.set("current_dashboard_number", dashboard_id)
|
||||
return {"status": "OK"}
|
||||
|
||||
async def get_slow_pipe_animation(self):
|
||||
slow_pipe_animation = self.cbpi.config.get("slow_pipe_animation", "Yes")
|
||||
return slow_pipe_animation
|
|
@ -8,6 +8,7 @@ import pandas as pd
|
|||
import zipfile
|
||||
import base64
|
||||
import urllib3
|
||||
from pathlib import Path
|
||||
from cbpi.api import *
|
||||
from cbpi.api.config import ConfigType
|
||||
from cbpi.api.base import CBPiBase
|
||||
|
@ -25,24 +26,26 @@ class LogController:
|
|||
self.logger = logging.getLogger(__name__)
|
||||
self.configuration = False
|
||||
self.datalogger = {}
|
||||
self.logsFolderPath = self.cbpi.config_folder.logsFolderPath
|
||||
self.logger.info("Log folder path : " + self.logsFolderPath)
|
||||
|
||||
def log_data(self, name: str, value: str) -> None:
|
||||
self.logfiles = self.cbpi.config.get("CSVLOGFILES", "Yes")
|
||||
self.influxdb = self.cbpi.config.get("INFLUXDB", "No")
|
||||
if self.logfiles == "Yes":
|
||||
if name not in self.datalogger:
|
||||
max_bytes = self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 1048576)
|
||||
backup_count = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3)
|
||||
max_bytes = int(self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", 100000))
|
||||
backup_count = int(self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", 3))
|
||||
|
||||
data_logger = logging.getLogger('cbpi.sensor.%s' % name)
|
||||
data_logger.propagate = False
|
||||
data_logger.setLevel(logging.DEBUG)
|
||||
handler = RotatingFileHandler('./logs/sensor_%s.log' % name, maxBytes=max_bytes, backupCount=backup_count)
|
||||
handler = RotatingFileHandler(os.path.join(self.logsFolderPath, f"sensor_{name}.log"), maxBytes=max_bytes, backupCount=backup_count)
|
||||
data_logger.addHandler(handler)
|
||||
self.datalogger[name] = data_logger
|
||||
|
||||
formatted_time = strftime("%Y-%m-%d %H:%M:%S", localtime())
|
||||
self.datalogger[name].info("%s,%s" % (formatted_time, value))
|
||||
self.datalogger[name].info("%s,%s" % (formatted_time, str(value)))
|
||||
if self.influxdb == "Yes":
|
||||
self.influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", "No")
|
||||
self.influxdbaddr = self.cbpi.config.get("INFLUXDBADDR", None)
|
||||
|
@ -115,8 +118,7 @@ class LogController:
|
|||
|
||||
for name in names:
|
||||
# get all log names
|
||||
all_filenames = glob.glob('./logs/sensor_%s.log*' % name)
|
||||
|
||||
all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*"))
|
||||
# concat all logs
|
||||
df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', name], header=None) for f in all_filenames])
|
||||
logging.info("Read all files for {}".format(names))
|
||||
|
@ -125,19 +127,29 @@ class LogController:
|
|||
df = df[name].resample(sample_rate).max()
|
||||
logging.info("Sampled now for {}".format(names))
|
||||
df = df.dropna()
|
||||
# take every nth row so that total number of rows does not exceed max_rows * 2
|
||||
max_rows = 500
|
||||
total_rows = df.shape[0]
|
||||
if (total_rows > 0) and (total_rows > max_rows):
|
||||
nth = int(total_rows/max_rows)
|
||||
if nth > 1:
|
||||
df = df.iloc[::nth]
|
||||
|
||||
if result is None:
|
||||
result = df
|
||||
else:
|
||||
result = pd.merge(result, df, how='outer', left_index=True, right_index=True)
|
||||
|
||||
data = {"time": df.index.tolist()}
|
||||
|
||||
|
||||
if len(names) > 1:
|
||||
for name in names:
|
||||
data[name] = result[name].interpolate(limit_direction='both', limit=10).tolist()
|
||||
else:
|
||||
data[name] = result.interpolate().tolist()
|
||||
|
||||
logging.info("Send Log for {}".format(names))
|
||||
|
||||
return data
|
||||
|
||||
async def get_data2(self, ids) -> dict:
|
||||
|
@ -146,7 +158,12 @@ class LogController:
|
|||
|
||||
result = dict()
|
||||
for id in ids:
|
||||
df = pd.read_csv("./logs/sensor_%s.log" % id, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime',"Values"], header=None)
|
||||
# df = pd.read_csv("./logs/sensor_%s.log" % id, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime',"Values"], header=None)
|
||||
# concat all logs
|
||||
all_filenames = glob.glob(os.path.join(self.logsFolderPath,f"sensor_{id}.log*"))
|
||||
df = pd.concat([pd.read_csv(f, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime', 'Values'], header=None) for f in all_filenames])
|
||||
df = df.resample('60s').max()
|
||||
df = df.dropna()
|
||||
result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()}
|
||||
return result
|
||||
|
||||
|
@ -159,11 +176,10 @@ class LogController:
|
|||
:return: list of log file names
|
||||
'''
|
||||
|
||||
return [os.path.basename(x) for x in glob.glob('./logs/sensor_%s.log*' % name)]
|
||||
return [os.path.basename(x) for x in glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*"))]
|
||||
|
||||
def clear_log(self, name:str ) -> str:
|
||||
|
||||
all_filenames = glob.glob('./logs/sensor_%s.log*' % name)
|
||||
all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*"))
|
||||
for f in all_filenames:
|
||||
os.remove(f)
|
||||
|
||||
|
@ -179,7 +195,7 @@ class LogController:
|
|||
:return:
|
||||
'''
|
||||
|
||||
return [os.path.basename(x) for x in glob.glob('./logs/*-sensor-%s.zip' % name)]
|
||||
return [os.path.basename(x) for x in glob.glob(os.path.join(self.logsFolderPath, f"*-sensor-{name}.zip"))]
|
||||
|
||||
def clear_zip(self, name:str ) -> None:
|
||||
"""
|
||||
|
@ -188,7 +204,7 @@ class LogController:
|
|||
:return: None
|
||||
"""
|
||||
|
||||
all_filenames = glob.glob('./logs/*-sensor-%s.zip' % name)
|
||||
all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"*-sensor-{name}.zip"))
|
||||
for f in all_filenames:
|
||||
os.remove(f)
|
||||
|
||||
|
@ -199,9 +215,9 @@ class LogController:
|
|||
"""
|
||||
|
||||
formatted_time = strftime("%Y-%m-%d-%H_%M_%S", localtime())
|
||||
file_name = './logs/%s-sensor-%s.zip' % (formatted_time, name)
|
||||
file_name = os.path.join(self.logsFolderPath, f"{formatted_time}-sensor-{name}.zip")
|
||||
zip = zipfile.ZipFile(file_name, 'w', zipfile.ZIP_DEFLATED)
|
||||
all_filenames = glob.glob('./logs/sensor_%s.log*' % name)
|
||||
all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*"))
|
||||
for f in all_filenames:
|
||||
zip.write(os.path.join(f))
|
||||
zip.close()
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import asyncio
|
||||
from email import message
|
||||
from cbpi.api.dataclasses import NotificationType
|
||||
from cbpi.api import *
|
||||
import logging
|
||||
import shortuuid
|
||||
class NotificationController:
|
||||
|
@ -10,9 +12,26 @@ class NotificationController:
|
|||
'''
|
||||
self.cbpi = cbpi
|
||||
self.logger = logging.getLogger(__name__)
|
||||
logging.root.addFilter(self.notify_log_event)
|
||||
self.callback_cache = {}
|
||||
self.listener = {}
|
||||
|
||||
def notify_log_event(self, record):
|
||||
NOTIFY_ON_ERROR = self.cbpi.config.get("NOTIFY_ON_ERROR", "No")
|
||||
if NOTIFY_ON_ERROR == "Yes":
|
||||
try:
|
||||
if record.levelno > 20:
|
||||
# on log events higher then INFO we want to notify all clients
|
||||
type = NotificationType.WARNING
|
||||
if record.levelno > 30:
|
||||
type = NotificationType.ERROR
|
||||
self.cbpi.notify(title=f"{record.levelname}", message=record.msg, type = type)
|
||||
except Exception as e:
|
||||
pass
|
||||
finally:
|
||||
return True
|
||||
return True
|
||||
|
||||
def add_listener(self, method):
|
||||
listener_id = shortuuid.uuid()
|
||||
self.listener[listener_id] = method
|
||||
|
|
|
@ -19,6 +19,8 @@ class SensorController(BasicController):
|
|||
return dict(name=data.get("name"), id=data.get("id"), type=data.get("type"), state=state,props=data.get("props", []))
|
||||
|
||||
def get_sensor_value(self, id):
|
||||
if id is None:
|
||||
return None
|
||||
try:
|
||||
return self.find_by_id(id).instance.get_state()
|
||||
except Exception as e:
|
||||
|
|
|
@ -56,9 +56,9 @@ class SystemController:
|
|||
output_filename="cbpi4_log.zip"
|
||||
|
||||
if logtime == "b":
|
||||
os.system('journalctl -b -u craftbeerpi.service > {}'.format(fullname))
|
||||
os.system('journalctl -b -u craftbeerpi.service --output cat > {}'.format(fullname))
|
||||
else:
|
||||
os.system('journalctl --since \"{} hours ago\" -u craftbeerpi.service > {}'.format(logtime, fullname))
|
||||
os.system('journalctl --since \"{} hours ago\" -u craftbeerpi.service --output cat > {}'.format(logtime, fullname))
|
||||
|
||||
os.system('cbpi plugins > {}'.format(fullpluginname))
|
||||
|
||||
|
@ -116,8 +116,8 @@ class SystemController:
|
|||
try:
|
||||
content = backup_file.read()
|
||||
if backup_file and self.allowed_file(filename, 'zip'):
|
||||
self.path = os.path.join(".", "restored_config.zip")
|
||||
|
||||
self.path = os.path.join(self.cbpi.config_folder.configFolderPath, "restored_config.zip")
|
||||
|
||||
f=open(self.path, "wb")
|
||||
f.write(content)
|
||||
f.close()
|
||||
|
|
|
@ -193,15 +193,21 @@ class UploadController:
|
|||
pass
|
||||
|
||||
# get the hop addition times
|
||||
c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze = 0 AND SudID = ?', (Recipe_ID,))
|
||||
c.execute('SELECT Zeit, Name FROM Hopfengaben WHERE Vorderwuerze <> 1 AND SudID = ?', (Recipe_ID,))
|
||||
hops = c.fetchall()
|
||||
whirlpool = []
|
||||
for hop in hops:
|
||||
if hop[0] < 0:
|
||||
whirlpool.append(hop)
|
||||
for whirl in whirlpool:
|
||||
hops.remove(whirl)
|
||||
|
||||
# get the misc addition times
|
||||
c.execute('SELECT Zugabedauer FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,))
|
||||
c.execute('SELECT Zugabedauer, Name FROM WeitereZutatenGaben WHERE Zeitpunkt = 1 AND SudID = ?', (Recipe_ID,))
|
||||
miscs = c.fetchall()
|
||||
|
||||
try:
|
||||
c.execute('SELECT Zeit FROM Hopfengaben WHERE Vorderwuerze = 1 AND SudID = ?', (Recipe_ID,))
|
||||
c.execute('SELECT Zeit, Name FROM Hopfengaben WHERE Vorderwuerze = 1 AND SudID = ?', (Recipe_ID,))
|
||||
FW_Hops = c.fetchall()
|
||||
FirstWort = self.getFirstWort(FW_Hops,"kbh")
|
||||
except:
|
||||
|
@ -291,14 +297,21 @@ class UploadController:
|
|||
"Sensor": self.boilkettle.sensor,
|
||||
"Temp": int(self.BoilTemp),
|
||||
"Timer": BoilTime,
|
||||
"First_Wort": FirstWort,
|
||||
"First_Wort": FirstWort[0],
|
||||
"First_Wort_text": FirstWort[1],
|
||||
"LidAlert": "Yes",
|
||||
"Hop_1": Hops[0],
|
||||
"Hop_2": Hops[1],
|
||||
"Hop_3": Hops[2],
|
||||
"Hop_4": Hops[3],
|
||||
"Hop_5": Hops[4],
|
||||
"Hop_6": Hops[5]
|
||||
"Hop_1": Hops[0][0],
|
||||
"Hop_1_text": Hops[0][1],
|
||||
"Hop_2": Hops[1][0],
|
||||
"Hop_2_text": Hops[1][1],
|
||||
"Hop_3": Hops[2][0],
|
||||
"Hop_3_text": Hops[2][1],
|
||||
"Hop_4": Hops[3][0],
|
||||
"Hop_4_text": Hops[3][1],
|
||||
"Hop_5": Hops[4][0],
|
||||
"Hop_5_text": Hops[4][1],
|
||||
"Hop_6": Hops[5][0],
|
||||
"Hop_6_text": Hops[5][1]
|
||||
},
|
||||
"status_text": "",
|
||||
"status": "I",
|
||||
|
@ -307,8 +320,11 @@ class UploadController:
|
|||
|
||||
await self.create_step(step_string)
|
||||
|
||||
await self.create_Whirlpool_Cooldown()
|
||||
|
||||
if not whirlpool:
|
||||
await self.create_Whirlpool_Cooldown()
|
||||
else :
|
||||
await self.create_Whirlpool_Cooldown(str(abs(whirlpool[0][0]))) # from kbh this value comes as negative but must be positive
|
||||
|
||||
self.cbpi.notify('KBH Recipe created', name, NotificationType.INFO)
|
||||
|
||||
except:
|
||||
|
@ -354,11 +370,12 @@ class UploadController:
|
|||
if e["Hopfen_{}_Kochzeit".format(idx)].isnumeric():
|
||||
if boil_time is not e["Hopfen_{}_Kochzeit".format(idx)].isnumeric():
|
||||
alert = float(e["Hopfen_{}_Kochzeit".format(idx)])
|
||||
elif e["Hopfen_{}_Kochzeit".format(idx)] == "Whirlpool":
|
||||
alert = float(1)
|
||||
elif e["Hopfen_{}_Kochzeit".format(idx)] == "Whirlpool" or float(e["Hopfen_{}_Kochzeit".format(idx)]) < 0:
|
||||
alert = float(0)
|
||||
hops_name = hops_name + ' whirlpool'
|
||||
else:
|
||||
self.api.notify(headline="No Number at Hoptime", message="Please change json-File at Hopfen_{}_Kochzeit".format(idx), type="danger")
|
||||
alert = float(1)
|
||||
self.cbpi.notify("No Number at Hoptime", "Please change json-File at Hopfen_{}_Kochzeit".format(idx), NotificationType.ERROR)
|
||||
alert = float(0)
|
||||
hops.append({"name":hops_name,"time":alert})
|
||||
|
||||
|
||||
|
@ -376,11 +393,12 @@ class UploadController:
|
|||
miscs_name = "%s%s %s" % (e["WeitereZutat_Wuerze_{}_Menge".format(idx)],e["WeitereZutat_Wuerze_{}_Einheit".format(idx)],e["WeitereZutat_Wuerze_{}_Name".format(idx)])
|
||||
if e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)].isnumeric():
|
||||
alert = float(e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)])
|
||||
elif e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)] == "Whirlpool":
|
||||
alert = float(1)
|
||||
elif e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)] == "Whirlpool" or float(e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)]) < 0:
|
||||
alert = float(0)
|
||||
miscs_name = miscs_name + ' whirlpool'
|
||||
else:
|
||||
self.api.notify(headline="No Number at Hoptime", message="Please change json-File at WeitereZutat_Wuerze_{}_Kochzeit".format(idx), type="danger")
|
||||
alert = float(1)
|
||||
alert = float(0)
|
||||
miscs.append({"name":miscs_name,"time":alert})
|
||||
|
||||
|
||||
|
@ -510,14 +528,21 @@ class UploadController:
|
|||
"Sensor": sensor,
|
||||
"Temp": step_temp,
|
||||
"Timer": step_time,
|
||||
"First_Wort": FirstWort,
|
||||
"First_Wort": FirstWort[0],
|
||||
"First_Wort_text": FirstWort[1],
|
||||
"LidAlert": LidAlert,
|
||||
"Hop_1": Hops[0],
|
||||
"Hop_2": Hops[1],
|
||||
"Hop_3": Hops[2],
|
||||
"Hop_4": Hops[3],
|
||||
"Hop_5": Hops[4],
|
||||
"Hop_6": Hops[5]
|
||||
"Hop_1": Hops[0][0],
|
||||
"Hop_1_text": Hops[0][1],
|
||||
"Hop_2": Hops[1][0],
|
||||
"Hop_2_text": Hops[1][1],
|
||||
"Hop_3": Hops[2][0],
|
||||
"Hop_3_text": Hops[2][1],
|
||||
"Hop_4": Hops[3][0],
|
||||
"Hop_4_text": Hops[3][1],
|
||||
"Hop_5": Hops[4][0],
|
||||
"Hop_5_text": Hops[4][1],
|
||||
"Hop_6": Hops[5][0],
|
||||
"Hop_6_text": Hops[5][1]
|
||||
},
|
||||
"status_text": "",
|
||||
"status": "I",
|
||||
|
@ -656,14 +681,21 @@ class UploadController:
|
|||
"Sensor": sensor,
|
||||
"Temp": step_temp,
|
||||
"Timer": step_time,
|
||||
"First_Wort": FirstWort,
|
||||
"First_Wort": FirstWort[0],
|
||||
"First_Wort_text": FirstWort[1],
|
||||
"LidAlert": LidAlert,
|
||||
"Hop_1": Hops[0],
|
||||
"Hop_2": Hops[1],
|
||||
"Hop_3": Hops[2],
|
||||
"Hop_4": Hops[3],
|
||||
"Hop_5": Hops[4],
|
||||
"Hop_6": Hops[5]
|
||||
"Hop_1": Hops[0][0],
|
||||
"Hop_1_text": Hops[0][1],
|
||||
"Hop_2": Hops[1][0],
|
||||
"Hop_2_text": Hops[1][1],
|
||||
"Hop_3": Hops[2][0],
|
||||
"Hop_3_text": Hops[2][1],
|
||||
"Hop_4": Hops[3][0],
|
||||
"Hop_4_text": Hops[3][1],
|
||||
"Hop_5": Hops[4][0],
|
||||
"Hop_5_text": Hops[4][1],
|
||||
"Hop_6": Hops[5][0],
|
||||
"Hop_6_text": Hops[5][1]
|
||||
},
|
||||
"status_text": "",
|
||||
"status": "I",
|
||||
|
@ -858,14 +890,21 @@ class UploadController:
|
|||
"Sensor": sensor,
|
||||
"Temp": step_temp,
|
||||
"Timer": step_time,
|
||||
"First_Wort": FirstWort,
|
||||
"First_Wort": FirstWort[0],
|
||||
"First_Wort_text": FirstWort[1],
|
||||
"LidAlert": LidAlert,
|
||||
"Hop_1": Hops[0],
|
||||
"Hop_2": Hops[1],
|
||||
"Hop_3": Hops[2],
|
||||
"Hop_4": Hops[3],
|
||||
"Hop_5": Hops[4],
|
||||
"Hop_6": Hops[5]
|
||||
"Hop_1": Hops[0][0],
|
||||
"Hop_1_text": Hops[0][1],
|
||||
"Hop_2": Hops[1][0],
|
||||
"Hop_2_text": Hops[1][1],
|
||||
"Hop_3": Hops[2][0],
|
||||
"Hop_3_text": Hops[2][1],
|
||||
"Hop_4": Hops[3][0],
|
||||
"Hop_4_text": Hops[3][1],
|
||||
"Hop_5": Hops[4][0],
|
||||
"Hop_5_text": Hops[4][1],
|
||||
"Hop_6": Hops[5][0],
|
||||
"Hop_6_text": Hops[5][1]
|
||||
},
|
||||
"status_text": "",
|
||||
"status": "I",
|
||||
|
@ -888,48 +927,51 @@ class UploadController:
|
|||
## Hops which are not used in the boil step should not cause alerts
|
||||
if use != 'Aroma' and use != 'Boil':
|
||||
continue
|
||||
alerts.append(float(hop.find('TIME').text))
|
||||
alerts.append([float(hop.find('TIME').text), hop.find('NAME').text])
|
||||
elif recipe_type == "bf":
|
||||
use = hop['use']
|
||||
if use != 'Aroma' and use != 'Boil':
|
||||
continue
|
||||
alerts.append(float(hop['time']))
|
||||
alerts.append([float(hop['time']), hop['name']]) ## TODO: Testing
|
||||
elif recipe_type == "kbh":
|
||||
alerts.append(float(hop[0]))
|
||||
alerts.append([float(hop[0]), hop[1]])
|
||||
elif recipe_type == "json":
|
||||
alerts.append(float(hop['time']))
|
||||
alerts.append([float(hop['time']), hop['name']])
|
||||
|
||||
## There might also be miscelaneous additions during boild time
|
||||
if miscs is not None:
|
||||
for misc in miscs:
|
||||
if recipe_type == "xml":
|
||||
alerts.append(float(misc.find('TIME').text))
|
||||
alerts.append([float(misc.find('TIME').text), misc.find('NAME').text])
|
||||
elif recipe_type == "bf":
|
||||
use = misc['use']
|
||||
if use != 'Aroma' and use != 'Boil':
|
||||
continue
|
||||
alerts.append(float(misc['time']))
|
||||
alerts.append([float(misc['time']), misc['name']]) ## TODO: Testing
|
||||
elif recipe_type == "kbh":
|
||||
alerts.append(float(misc[0]))
|
||||
alerts.append([float(misc[0]), misc[1]])
|
||||
elif recipe_type == "json":
|
||||
alerts.append(float(misc['time']))
|
||||
## Dedupe and order the additions by their time, to prevent multiple alerts at the same time
|
||||
alerts = sorted(list(set(alerts)))
|
||||
alerts.append([float(misc['time']), misc['name']])
|
||||
## Dedupe and order the additions by their time
|
||||
## CBP should have these additions in reverse
|
||||
alerts.reverse()
|
||||
hop_alerts = []
|
||||
alerts = sorted(alerts, key=lambda x:x[0], reverse=True)
|
||||
hop_alerts = [[None, None],[None, None],[None, None],[None, None],[None, None],[None, None]]
|
||||
for i in range(0,6):
|
||||
try:
|
||||
hop_alerts.append(str(int(alerts[i])))
|
||||
if float(alerts[i][0]) > -1:
|
||||
hop_alerts[i] = alerts[i]
|
||||
except:
|
||||
hop_alerts.append(None)
|
||||
pass
|
||||
return hop_alerts
|
||||
|
||||
def getFirstWort(self, hops, recipe_type):
|
||||
alert = "No"
|
||||
names = []
|
||||
if recipe_type == "kbh":
|
||||
if len(hops) != 0:
|
||||
alert = "Yes"
|
||||
for hop in hops:
|
||||
names.append(hop[1])
|
||||
elif recipe_type == "xml":
|
||||
for hop in hops:
|
||||
use = hop.find('USE').text
|
||||
|
@ -937,22 +979,27 @@ class UploadController:
|
|||
if use != 'First Wort':
|
||||
continue
|
||||
alert = "Yes"
|
||||
names.append(hop.find('NAME').text)
|
||||
elif recipe_type == "bf":
|
||||
for hop in hops:
|
||||
if hop['use'] == "First Wort":
|
||||
alert="Yes"
|
||||
names.append(hop['name']) ## TODO: Testing
|
||||
elif recipe_type == "json":
|
||||
for hop in hops:
|
||||
alert="Yes"
|
||||
return alert
|
||||
if len(hops) != 0:
|
||||
alert = "Yes"
|
||||
for hop in hops:
|
||||
names.append(hop['name'])
|
||||
|
||||
return [alert, " and ".join(names)]
|
||||
|
||||
async def create_Whirlpool_Cooldown(self):
|
||||
async def create_Whirlpool_Cooldown(self, time : str = "15"):
|
||||
# Add Waitstep as Whirlpool
|
||||
if self.cooldown != "WaiStep" and self.cooldown !="":
|
||||
step_string = { "name": "Whirlpool",
|
||||
"props": {
|
||||
"Kettle": self.boilid,
|
||||
"Timer": "15"
|
||||
"Timer": time
|
||||
},
|
||||
"status_text": "",
|
||||
"status": "I",
|
||||
|
@ -965,7 +1012,7 @@ class UploadController:
|
|||
step_name = "CoolDown"
|
||||
cooldown_sensor = ""
|
||||
step_temp = ""
|
||||
step_timer = "15"
|
||||
step_timer = time
|
||||
if step_type == "CooldownStep":
|
||||
cooldown_sensor = self.cbpi.config.get("steps_cooldown_sensor", None)
|
||||
if cooldown_sensor is None or cooldown_sensor == '':
|
||||
|
|
|
@ -297,16 +297,10 @@ class CraftBeerPi:
|
|||
await self.kettle.init()
|
||||
await self.call_initializer(self.app)
|
||||
await self.dashboard.init()
|
||||
|
||||
|
||||
|
||||
self._swagger_setup()
|
||||
|
||||
level = logging.INFO
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(level)
|
||||
for handler in logger.handlers:
|
||||
handler.setLevel(level)
|
||||
|
||||
return self.app
|
||||
|
||||
def start(self):
|
||||
|
|
|
@ -47,8 +47,11 @@ class ConfigUpdate(CBPiExtension):
|
|||
influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None)
|
||||
mqttupdate = self.cbpi.config.get("MQTTUpdate", None)
|
||||
PRESSURE_UNIT = self.cbpi.config.get("PRESSURE_UNIT", None)
|
||||
|
||||
|
||||
SENSOR_LOG_BACKUP_COUNT = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", None)
|
||||
SENSOR_LOG_MAX_BYTES = self.cbpi.config.get("SENSOR_LOG_MAX_BYTES", None)
|
||||
slow_pipe_animation = self.cbpi.config.get("slow_pipe_animation", None)
|
||||
NOTIFY_ON_ERROR = self.cbpi.config.get("NOTIFY_ON_ERROR", None)
|
||||
|
||||
if boil_temp is None:
|
||||
logger.info("INIT Boil Temp Setting")
|
||||
try:
|
||||
|
@ -285,6 +288,42 @@ class ConfigUpdate(CBPiExtension):
|
|||
{"label": "PSI", "value": "PSI"}])
|
||||
except:
|
||||
logger.warning('Unable to update config')
|
||||
|
||||
# check if SENSOR_LOG_BACKUP_COUNT exists in config
|
||||
if SENSOR_LOG_BACKUP_COUNT is None:
|
||||
logger.info("INIT SENSOR_LOG_BACKUP_COUNT")
|
||||
try:
|
||||
await self.cbpi.config.add("SENSOR_LOG_BACKUP_COUNT", 3, ConfigType.NUMBER, "Max. number of backup logs")
|
||||
except:
|
||||
logger.warning('Unable to update database')
|
||||
|
||||
# check if SENSOR_LOG_MAX_BYTES exists in config
|
||||
if SENSOR_LOG_MAX_BYTES is None:
|
||||
logger.info("Init maximum size of sensor logfiles")
|
||||
try:
|
||||
await self.cbpi.config.add("SENSOR_LOG_MAX_BYTES", 100000, ConfigType.NUMBER, "Max. number of bytes in sensor logs")
|
||||
except:
|
||||
logger.warning('Unable to update database')
|
||||
|
||||
# Check if slow_pipe_animation is in config
|
||||
if slow_pipe_animation is None:
|
||||
logger.info("INIT slow_pipe_animation")
|
||||
try:
|
||||
await self.cbpi.config.add("slow_pipe_animation", "Yes", ConfigType.SELECT, "Slow down dashboard pipe animation taking up close to 100% of the CPU's capacity",
|
||||
[{"label": "Yes", "value": "Yes"},
|
||||
{"label": "No", "value": "No"}])
|
||||
except:
|
||||
logger.warning('Unable to update config')
|
||||
|
||||
## Check if NOTIFY_ON_ERROR is in config
|
||||
if NOTIFY_ON_ERROR is None:
|
||||
logger.info("INIT NOTIFY_ON_ERROR")
|
||||
try:
|
||||
await self.cbpi.config.add("NOTIFY_ON_ERROR", "No", ConfigType.SELECT, "Send Notification on Logging Error",
|
||||
[{"label": "Yes", "value": "Yes"},
|
||||
{"label": "No", "value": "No"}])
|
||||
except:
|
||||
logger.warning('Unable to update config')
|
||||
|
||||
def setup(cbpi):
|
||||
cbpi.plugin.register("ConfigUpdate", ConfigUpdate)
|
||||
|
|
|
@ -322,12 +322,19 @@ class ActorStep(CBPiStep):
|
|||
Property.Select(label="LidAlert",options=["Yes","No"], description="Trigger Alert to remove lid if temp is close to boil"),
|
||||
Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Kettlelogic automatically on and off -> Yes"),
|
||||
Property.Select("First_Wort", options=["Yes","No"], description="First Wort Hop alert if set to Yes"),
|
||||
Property.Text("First_Wort_text", configurable = True, description="First Wort Hop alert text"),
|
||||
Property.Number("Hop_1", configurable = True, description="First Hop alert (minutes before finish)"),
|
||||
Property.Text("Hop_1_text", configurable = True, description="First Hop alert text"),
|
||||
Property.Number("Hop_2", configurable=True, description="Second Hop alert (minutes before finish)"),
|
||||
Property.Text("Hop_2_text", configurable = True, description="Second Hop alert text"),
|
||||
Property.Number("Hop_3", configurable=True, description="Third Hop alert (minutes before finish)"),
|
||||
Property.Text("Hop_3_text", configurable = True, description="Third Hop alert text"),
|
||||
Property.Number("Hop_4", configurable=True, description="Fourth Hop alert (minutes before finish)"),
|
||||
Property.Text("Hop_4_text", configurable = True, description="Fourth Hop alert text"),
|
||||
Property.Number("Hop_5", configurable=True, description="Fifth Hop alert (minutes before finish)"),
|
||||
Property.Number("Hop_6", configurable=True, description="Sixth Hop alert (minutes before finish)")])
|
||||
Property.Text("Hop_5_text", configurable = True, description="Fifth Hop alert text"),
|
||||
Property.Number("Hop_6", configurable=True, description="Sixth Hop alert (minutes before finish)"),
|
||||
Property.Text("Hop_6_text", configurable = True, description="Sixth Hop alert text")])
|
||||
class BoilStep(CBPiStep):
|
||||
|
||||
@action("Start Timer", [])
|
||||
|
@ -367,6 +374,7 @@ class BoilStep(CBPiStep):
|
|||
self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False
|
||||
self.first_wort_hop_flag = False
|
||||
self.first_wort_hop=self.props.get("First_Wort", "No")
|
||||
self.first_wort_hop_text=self.props.get("First_Wort_text", None)
|
||||
self.hops_added=["","","","","",""]
|
||||
self.remaining_seconds = None
|
||||
|
||||
|
@ -389,11 +397,14 @@ class BoilStep(CBPiStep):
|
|||
await self.setAutoMode(True)
|
||||
await self.push_update()
|
||||
|
||||
async def check_hop_timer(self, number, value):
|
||||
async def check_hop_timer(self, number, value, text):
|
||||
if value is not None and self.hops_added[number-1] is not True:
|
||||
if self.remaining_seconds != None and self.remaining_seconds <= (int(value) * 60 + 1):
|
||||
self.hops_added[number-1]= True
|
||||
self.cbpi.notify('Hop Alert', "Please add Hop %s" % number, NotificationType.INFO)
|
||||
if text is not None and text != "":
|
||||
self.cbpi.notify('Hop Alert', "Please add %s (%s)" % (text, number), NotificationType.INFO)
|
||||
else:
|
||||
self.cbpi.notify('Hop Alert', "Please add Hop %s" % number, NotificationType.INFO)
|
||||
|
||||
async def on_stop(self):
|
||||
await self.timer.stop()
|
||||
|
@ -409,7 +420,10 @@ class BoilStep(CBPiStep):
|
|||
async def run(self):
|
||||
if self.first_wort_hop_flag == False and self.first_wort_hop == "Yes":
|
||||
self.first_wort_hop_flag = True
|
||||
self.cbpi.notify('First Wort Hop Addition!', 'Please add hops for first wort', NotificationType.INFO)
|
||||
if self.first_wort_hop_text is not None and self.first_wort_hop_text != "":
|
||||
self.cbpi.notify('First Wort Hop Addition!', 'Please add %s for first wort' % self.first_wort_hop_text, NotificationType.INFO)
|
||||
else:
|
||||
self.cbpi.notify('First Wort Hop Addition!', 'Please add hops for first wort', NotificationType.INFO)
|
||||
|
||||
while self.running == True:
|
||||
await asyncio.sleep(1)
|
||||
|
@ -426,7 +440,7 @@ class BoilStep(CBPiStep):
|
|||
self.cbpi.notify(self.name, 'Timer started. Estimated completion: {}'.format(estimated_completion_time.strftime("%H:%M")), NotificationType.INFO)
|
||||
else:
|
||||
for x in range(1, 6):
|
||||
await self.check_hop_timer(x, self.props.get("Hop_%s" % x, None))
|
||||
await self.check_hop_timer(x, self.props.get("Hop_%s" % x, None), self.props.get("Hop_%s_text" % x, None))
|
||||
|
||||
return StepResult.DONE
|
||||
|
||||
|
|
|
@ -32,6 +32,6 @@ class GenericMqttActor(MQTTActor):
|
|||
self.state = True
|
||||
|
||||
async def off(self):
|
||||
formatted_payload = self.payload.format(switch_onoff = "off", switch_10 = 0, power = self.power)
|
||||
formatted_payload = self.payload.format(switch_onoff = "off", switch_10 = 0, power = 0)
|
||||
await self.publish_mqtt_message(self.topic, formatted_payload)
|
||||
self.state = False
|
||||
self.state = False
|
||||
|
|
|
@ -39,7 +39,7 @@ class MQTTActor(CBPiActor):
|
|||
async def off(self):
|
||||
self.state = False
|
||||
await self.cbpi.satellite.publish(self.topic, json.dumps(
|
||||
{"state": "off", "power": self.power}), True)
|
||||
{"state": "off", "power": 0}), True)
|
||||
pass
|
||||
|
||||
async def run(self):
|
||||
|
@ -56,4 +56,4 @@ class MQTTActor(CBPiActor):
|
|||
else:
|
||||
await self.off()
|
||||
await self.cbpi.actor.actor_update(self.id,power)
|
||||
pass
|
||||
pass
|
||||
|
|
|
@ -24,6 +24,21 @@ class ActorHttpEndpoints():
|
|||
"""
|
||||
return web.json_response(data=self.controller.get_state())
|
||||
|
||||
@request_mapping(path="/ws_update", auth_required=False)
|
||||
async def http_get_all(self, request):
|
||||
"""
|
||||
|
||||
---
|
||||
description: Update WS actors
|
||||
tags:
|
||||
- Actor
|
||||
responses:
|
||||
"204":
|
||||
description: successful operation
|
||||
"""
|
||||
return web.json_response(data=await self.controller.ws_actor_update())
|
||||
|
||||
|
||||
@request_mapping(path="/{id:\w+}", auth_required=False)
|
||||
async def http_get_one(self, request):
|
||||
"""
|
||||
|
|
|
@ -157,3 +157,15 @@ class DashBoardHttpEndpoints:
|
|||
dashboard_id = int(request.match_info['id'])
|
||||
return web.json_response(await self.cbpi.dashboard.set_current_dashboard(dashboard_id))
|
||||
|
||||
@request_mapping(path="/slowPipeAnimation", method="GET", auth_required=False)
|
||||
async def get_slow_pipe_animation(self, request):
|
||||
"""
|
||||
---
|
||||
description: Get slow down dashboard pipe animation (Yes/No)
|
||||
tags:
|
||||
- Dashboard
|
||||
responses:
|
||||
"200":
|
||||
description: successful operation
|
||||
"""
|
||||
return web.json_response(await self.cbpi.dashboard.get_slow_pipe_animation(), dumps=json_dumps)
|
||||
|
|
|
@ -2,6 +2,7 @@ from cbpi.utils.encoder import ComplexEncoder
|
|||
from aiohttp import web
|
||||
from cbpi.utils.utils import json_dumps
|
||||
from cbpi.api import request_mapping
|
||||
import os
|
||||
import json
|
||||
class LogHttpEndpoints:
|
||||
|
||||
|
@ -83,7 +84,7 @@ class LogHttpEndpoints:
|
|||
)
|
||||
await response.prepare(request)
|
||||
log_name = request.match_info['name']
|
||||
with open('./logs/%s.zip' % log_name, 'rb') as file:
|
||||
with open(os.path.join(self.cbpi.logsFolderPath, '%s.zip' % log_name), 'rb') as file:
|
||||
for line in file.readlines():
|
||||
await response.write(line)
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ class SystemHttpEndpoints:
|
|||
async def http_get_log(self, request):
|
||||
result = []
|
||||
file_pattern = re.compile("^(\w+.).log(.?\d*)")
|
||||
for filename in sorted(os.listdir("./logs"), reverse=True): #
|
||||
for filename in sorted(os.listdir(self.cbpi.logsFolderPath), reverse=True):
|
||||
if file_pattern.match(filename):
|
||||
result.append(filename)
|
||||
return web.json_response(result)
|
||||
|
|
|
@ -21,9 +21,9 @@ class Scheduler(*bases):
|
|||
self._close_timeout = close_timeout
|
||||
self._limit = limit
|
||||
self._exception_handler = exception_handler
|
||||
self._failed_tasks = asyncio.Queue(loop=loop)
|
||||
self._failed_tasks = asyncio.Queue()
|
||||
self._failed_task = loop.create_task(self._wait_failed())
|
||||
self._pending = asyncio.Queue(maxsize=pending_limit, loop=loop)
|
||||
self._pending = asyncio.Queue(maxsize=pending_limit)
|
||||
self._closed = False
|
||||
|
||||
def __iter__(self):
|
||||
|
|
|
@ -19,5 +19,6 @@ class CraftBeerPiTestCase(AioHTTPTestCase):
|
|||
def configuration(self):
|
||||
test_directory = os.path.dirname(__file__)
|
||||
test_config_directory = os.path.join(test_directory, 'cbpi-test-config')
|
||||
configFolder = ConfigFolder(test_config_directory)
|
||||
test_logs_directory = os.path.join(test_directory, 'logs')
|
||||
configFolder = ConfigFolder(test_config_directory, test_logs_directory)
|
||||
return configFolder
|
||||
|
|
|
@ -9,7 +9,7 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
|
|||
class CLITest(unittest.TestCase):
|
||||
|
||||
def test_list(self):
|
||||
cli = CraftBeerPiCli(ConfigFolder("./cbpi-test-config"))
|
||||
cli = CraftBeerPiCli(ConfigFolder("./cbpi-test-config", './logs')) # inside tests folder
|
||||
cli.plugins_list()
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -10,11 +10,11 @@ class LoggerTestCase(CraftBeerPiTestCase):
|
|||
@unittest_run_loop
|
||||
async def test_log_data(self):
|
||||
|
||||
os.makedirs("./logs", exist_ok=True)
|
||||
os.makedirs(os.path.join(".", "tests", "logs"), exist_ok=True)
|
||||
log_name = "test"
|
||||
#clear all logs
|
||||
self.cbpi.log.clear_log(log_name)
|
||||
assert len(glob.glob('./logs/sensor_%s.log*' % log_name)) == 0
|
||||
assert len(glob.glob(os.path.join(".", "tests", "logs", f"sensor_{log_name}.log*"))) == 0
|
||||
|
||||
# write log entries
|
||||
for i in range(5):
|
||||
|
|
13
testversion.py
Normal file
13
testversion.py
Normal 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')
|
Loading…
Reference in a new issue