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/*
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 \

View file

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

View file

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

View file

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

3
.gitignore vendored
View file

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

29
.vscode/launch.json vendored
View file

@ -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
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"
__codename__ = "Spring Break"
__version__ = "4.0.7"
__codename__ = "November Rain"

View file

@ -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
@ -98,7 +101,9 @@ class CraftBeerPiCli():
with ZipFile('temp.zip', 'r') as repo_zip:
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))
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

View file

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

View file

@ -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,7 +59,7 @@ 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":
@ -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")

View file

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

View file

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

View file

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

View file

@ -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,6 +127,14 @@ 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:
@ -137,7 +147,9 @@ class LogController:
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()

View file

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

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", []))
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:

View file

@ -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,7 +116,7 @@ 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)

View file

@ -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,7 +320,10 @@ 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)
@ -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'])
async def create_Whirlpool_Cooldown(self):
return [alert, " and ".join(names)]
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 == '':

View file

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

View file

@ -47,7 +47,10 @@ 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")
@ -286,6 +289,42 @@ class ConfigUpdate(CBPiExtension):
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)
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="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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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__':

View file

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