diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 41c4890..f1d8ce0 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/vscode/devcontainers/python:3.9-bullseye +FROM mcr.microsoft.com/vscode/devcontainers/python:3.11-bullseye RUN apt-get update \ && apt-get upgrade -y diff --git a/Dockerfile b/Dockerfile index 23fa44e..69b9e0c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ RUN apk --no-cache add curl && mkdir /downloads # Download installation files RUN curl https://github.com/avollkopf/craftbeerpi4-ui/archive/main.zip -L -o ./downloads/cbpi-ui.zip -FROM python:3.9 as base +FROM python:3.10 as base # Install dependencies RUN apt-get update \ diff --git a/cbpi/__init__.py b/cbpi/__init__.py index c4435e6..efd5ddd 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.0.7" -__codename__ = "November Rain" +__version__ = "4.1.0" +__codename__ = "Groundhog Day" diff --git a/cbpi/cli.py b/cbpi/cli.py index a0d783e..030d944 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -15,7 +15,7 @@ from colorama import Fore, Back, Style import importlib from importlib_metadata import metadata from tabulate import tabulate -from PyInquirer import prompt, print_json +from inquirer import prompt import platform import time diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index be01fc0..807550b 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -8,7 +8,7 @@ import shutil import zipfile from pathlib import Path import glob - +import json class ConfigFolder: def __init__(self, configFolderPath, logsFolderPath): @@ -91,8 +91,8 @@ class ConfigFolder: ['actor.json', 'file'], ['sensor.json', 'file'], ['kettle.json', 'file'], - ['fermenter_data.json', 'file'], - ['step_data.json', 'file'], + #['fermenter_data.json', 'file'], created by fermentation_controller @ start if not available + #['step_data.json', 'file'], created by step_controller @ start if not available ['config.json', 'file'], ['craftbeerpi.service', 'file'], ['chromium.desktop', 'file'], @@ -121,9 +121,23 @@ class ConfigFolder: # 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'))): + dashboard_1_path = os.path.join(self.configFolderPath, 'dashboard', 'cbpi_dashboard_1.json') + if (not (os.path.isfile(dashboard_1_path))) or self.check_for_empty_dashboard_1(dashboard_1_path): 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))) + dashboardFile = os.path.basename(file) + print(f"Copy dashboard json file {dashboardFile} from config to config/dashboard folder") + shutil.move(file, os.path.join(self.configFolderPath, 'dashboard', dashboardFile)) + + def check_for_empty_dashboard_1(self, dashboard_1_path): + try: + with open(dashboard_1_path, 'r') as f: + data = json.load(f) + if (len(data['elements']) == 0): # there may exist some pathes but pathes without elements in dashboard is not very likely + return True + else: + return False + except: # file missing or bad json format + return True def inform_missing_content(self, whatsmissing : str): if whatsmissing == "": diff --git a/cbpi/controller/dashboard_controller.py b/cbpi/controller/dashboard_controller.py index feef806..2bb77b3 100644 --- a/cbpi/controller/dashboard_controller.py +++ b/cbpi/controller/dashboard_controller.py @@ -34,7 +34,7 @@ class DashboardController: return {'elements': [], 'pathes': []} async def add_content(self, dashboard_id, data): - print(data) + #print(data) 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) diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index 8fa5de2..9dbc6ba 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -36,7 +36,7 @@ class FermentationController: def check_fermenter_file(self): if os.path.exists(self.cbpi.config_folder.get_file_path("fermenter_data.json")) is False: - logging.info("INIT fermenter_data.json file") + logging.warning("Missing fermenter_data.json file. INIT empty file") data = { "data": [ ] @@ -71,11 +71,23 @@ class FermentationController: async def load(self): - with open(self.path) as json_file: - data = json.load(json_file) + try: + with open(self.path) as json_file: + data = json.load(json_file) + for i in data["data"]: + self.data.append(self._create(i)) + except: + logging.warning("Invalid fermenter_data.json file - Creating empty file") + os.remove(self.path) + data = { + "data": [ + ] + } + destfile = self.cbpi.config_folder.get_file_path("fermenter_data.json") + json.dump(data,open(destfile,'w'),indent=4, sort_keys=True) for i in data["data"]: - self.data.append(self._create(i)) + self.data.append(self._create(i)) def _create_step(self, fermenter, item): id = item.get("id") diff --git a/cbpi/controller/log_file_controller.py b/cbpi/controller/log_file_controller.py index 50790e2..9e8c9a0 100644 --- a/cbpi/controller/log_file_controller.py +++ b/cbpi/controller/log_file_controller.py @@ -53,6 +53,7 @@ class LogController: self.influxdbname = self.cbpi.config.get("INFLUXDBNAME", None) self.influxdbuser = self.cbpi.config.get("INFLUXDBUSER", None) self.influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None) + self.influxdbmeasurement = self.cbpi.config.get("INFLUXDBMEASUREMENT", "measurement") id = name try: @@ -62,7 +63,7 @@ class LogController: itemname=sensor.name.replace(" ", "_") for char in chars: itemname = itemname.replace(char,chars[char]) - out="measurement,source=" + itemname + ",itemID=" + str(id) + " value="+str(value) + out=str(self.influxdbmeasurement)+",source=" + itemname + ",itemID=" + str(id) + " value="+str(value) except Exception as e: logging.error("InfluxDB ID Error: {}".format(e)) @@ -153,15 +154,12 @@ class LogController: return data async def get_data2(self, ids) -> dict: - def dateparse(time_in_secs): - return datetime.datetime.strptime(time_in_secs, '%Y-%m-%d %H:%M:%S') + dateparse = lambda dates: [datetime.datetime.strptime(d, '%Y-%m-%d %H:%M:%S') for d in dates] 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) - # 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 = pd.concat([pd.read_csv(f, parse_dates=['DateTime'], 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()} @@ -180,12 +178,18 @@ class LogController: def clear_log(self, name:str ) -> str: all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*")) - for f in all_filenames: - os.remove(f) if name in self.datalogger: + self.datalogger[name].removeHandler(self.datalogger[name].handlers[0]) del self.datalogger[name] + for f in all_filenames: + try: + os.remove(f) + except Exception as e: + logging.warning(e) + + def get_all_zip_file_names(self, name: str) -> list: diff --git a/cbpi/controller/notification_controller.py b/cbpi/controller/notification_controller.py index eb105d6..1040bf0 100644 --- a/cbpi/controller/notification_controller.py +++ b/cbpi/controller/notification_controller.py @@ -45,11 +45,11 @@ class NotificationController: async def _call_listener(self, title, message, type, action): for id, method in self.listener.items(): - print(id, method) + #print(id, method) asyncio.create_task(method(self.cbpi, title, message, type, action )) - def notify(self, title, message: str, type: NotificationType = NotificationType.INFO, action=[]) -> None: + def notify(self, title, message: str, type: NotificationType = NotificationType.INFO, action=[], timeout: int=5000) -> None: ''' This is a convinience method to send notification to the client @@ -66,8 +66,8 @@ class NotificationController: actions = list(map(lambda item: prepare_action(item), action)) self.callback_cache[notifcation_id] = action - self.cbpi.ws.send(dict(id=notifcation_id, topic="notifiaction", type=type.value, title=title, message=message, action=actions)) - data = dict(type=type.value, title=title, message=message, action=actions) + self.cbpi.ws.send(dict(id=notifcation_id, topic="notifiaction", type=type.value, title=title, message=message, action=actions, timeout=timeout)) + data = dict(type=type.value, title=title, message=message, action=actions, timeout=timeout) self.cbpi.push_update(topic="cbpi/notification", data=data) asyncio.create_task(self._call_listener(title, message, type, action)) diff --git a/cbpi/controller/plugin_controller.py b/cbpi/controller/plugin_controller.py index 26081e6..e958371 100644 --- a/cbpi/controller/plugin_controller.py +++ b/cbpi/controller/plugin_controller.py @@ -190,14 +190,14 @@ class PluginController(): return result - async def load_plugin_list(self): + async def load_plugin_list(self, filter="cbpi"): result = [] try: discovered_plugins = { name: importlib.import_module(name) for finder, name, ispkg in pkgutil.iter_modules() - if name.startswith('cbpi') and len(name) > 4 + if name.startswith(filter) and len(name) > 4 } for key, module in discovered_plugins.items(): from importlib.metadata import version diff --git a/cbpi/controller/satellite_controller.py b/cbpi/controller/satellite_controller.py index 230193e..d11885e 100644 --- a/cbpi/controller/satellite_controller.py +++ b/cbpi/controller/satellite_controller.py @@ -2,14 +2,16 @@ import asyncio import json from re import M -from asyncio_mqtt import Client, MqttError, Will, client +from asyncio_mqtt import Client, MqttError, Will from contextlib import AsyncExitStack, asynccontextmanager from cbpi import __version__ import logging +import shortuuid class SatelliteController: def __init__(self, cbpi): + self.client_id = shortuuid.uuid() self.cbpi = cbpi self.kettlecontroller = cbpi.kettle self.fermentercontroller = cbpi.fermenter @@ -34,7 +36,46 @@ class SatelliteController: self.tasks = set() async def init(self): - asyncio.create_task(self.init_client(self.cbpi)) + + #not sure if required like done in the old routine + async def cancel_tasks(tasks): + for task in tasks: + if task.done(): + continue + task.cancel() + try: + await task + except asyncio.CancelledError: + pass + + self.client = Client(self.host, port=self.port, username=self.username, password=self.password, will=Will(topic="cbpi/disconnect", payload="CBPi Server Disconnected"),client_id=self.client_id) + self.loop = asyncio.get_event_loop() + ## Listen for mqtt messages in an (unawaited) asyncio task + task = self.loop.create_task(self.listen()) + ## Save a reference to the task so it doesn't get garbage collected + self.tasks.add(task) + task.add_done_callback(self.tasks.remove) + + self.logger.info("MQTT Connected to {}:{}".format(self.host, self.port)) + + async def listen(self): + while True: + try: + async with self.client as client: + async with client.messages() as messages: + await client.subscribe("#") + async for message in messages: + for topic_filter in self.topic_filters: + topic = topic_filter[0] + method = topic_filter[1] + if message.topic.matches(topic): + await (method(message)) + except MqttError as e: + self.logger.error("MQTT Exception: {}".format(e)) + except Exception as e: + self.logger.error("MQTT General Exception: {}".format(e)) + await asyncio.sleep(5) + async def publish(self, topic, message, retain=False): if self.client is not None and self.client._connected: @@ -43,26 +84,25 @@ class SatelliteController: except Exception as e: self.logger.warning("Failed to push data via mqtt: {}".format(e)) - async def _actor_on(self, messages): - async for message in messages: + async def _actor_on(self, message): try: - topic_key = message.topic.split("/") + topic_key = str(message.topic).split("/") await self.cbpi.actor.on(topic_key[2]) + self.logger.warning("Processed actor {} on via mqtt".format(topic_key[2])) except Exception as e: self.logger.warning("Failed to process actor on via mqtt: {}".format(e)) - async def _actor_off(self, messages): - async for message in messages: + async def _actor_off(self, message): try: - topic_key = message.topic.split("/") + topic_key = str(message.topic).split("/") await self.cbpi.actor.off(topic_key[2]) + self.logger.warning("Processed actor {} off via mqtt".format(topic_key[2])) except Exception as e: self.logger.warning("Failed to process actor off via mqtt: {}".format(e)) - async def _actor_power(self, messages): - async for message in messages: + async def _actor_power(self, message): try: - topic_key = message.topic.split("/") + topic_key = str(message.topic).split("/") try: power=int(message.payload.decode()) if power > 100: @@ -76,8 +116,7 @@ class SatelliteController: except: self.logger.warning("Failed to set actor power via mqtt") - async def _kettleupdate(self, messages): - async for message in messages: + async def _kettleupdate(self, message): try: self.kettle=self.kettlecontroller.get_state() for item in self.kettle['data']: @@ -85,8 +124,7 @@ class SatelliteController: except Exception as e: self.logger.warning("Failed to send kettleupdate via mqtt: {}".format(e)) - async def _fermenterupdate(self, messages): - async for message in messages: + async def _fermenterupdate(self, message): try: self.fermenter=self.fermentercontroller.get_state() for item in self.fermenter['data']: @@ -94,8 +132,7 @@ class SatelliteController: except Exception as e: self.logger.warning("Failed to send fermenterupdate via mqtt: {}".format(e)) - async def _actorupdate(self, messages): - async for message in messages: + async def _actorupdate(self, message): try: self.actor=self.actorcontroller.get_state() for item in self.actor['data']: @@ -103,8 +140,7 @@ class SatelliteController: except Exception as e: self.logger.warning("Failed to send actorupdate via mqtt: {}".format(e)) - async def _sensorupdate(self, messages): - async for message in messages: + async def _sensorupdate(self, message): try: self.sensor=self.sensorcontroller.get_state() for item in self.sensor['data']: @@ -120,10 +156,11 @@ class SatelliteController: while True: try: if self.client._connected.done(): - async with self.client.filtered_messages(topic) as messages: + async with self.client.messages() as messages: await self.client.subscribe(topic) async for message in messages: - await method(message.payload.decode()) + if message.topic.matches(topic): + await method(message.payload.decode()) except asyncio.CancelledError: # Cancel self.logger.warning("Sub Cancelled") @@ -134,45 +171,3 @@ class SatelliteController: # wait before try to resubscribe await asyncio.sleep(5) - - async def init_client(self, cbpi): - - async def cancel_tasks(tasks): - for task in tasks: - if task.done(): - continue - task.cancel() - try: - await task - except asyncio.CancelledError: - pass - - while True: - try: - async with AsyncExitStack() as stack: - self.tasks = set() - stack.push_async_callback(cancel_tasks, self.tasks) - self.client = Client(self.host, port=self.port, username=self.username, password=self.password, will=Will(topic="cbpi/disconnect", payload="CBPi Server Disconnected")) - - await stack.enter_async_context(self.client) - - for topic_filter in self.topic_filters: - topic = topic_filter[0] - method = topic_filter[1] - manager = self.client.filtered_messages(topic) - messages = await stack.enter_async_context(manager) - task = asyncio.create_task(method(messages)) - self.tasks.add(task) - - for topic_filter in self.topic_filters: - topic = topic_filter[0] - await self.client.subscribe(topic) - - self.logger.info("MQTT Connected to {}:{}".format(self.host, self.port)) - await asyncio.gather(*self.tasks) - - except MqttError as e: - self.logger.error("MQTT Exception: {}".format(e)) - except Exception as e: - self.logger.error("MQTT General Exception: {}".format(e)) - await asyncio.sleep(5) diff --git a/cbpi/controller/step_controller.py b/cbpi/controller/step_controller.py index 8850fd9..651b3fb 100644 --- a/cbpi/controller/step_controller.py +++ b/cbpi/controller/step_controller.py @@ -6,6 +6,7 @@ import yaml import logging import os.path from os import listdir +import os from os.path import isfile, join import shortuuid from cbpi.api.dataclasses import NotificationAction, Props, Step @@ -54,23 +55,42 @@ class StepController: # create file if not exists if os.path.exists(self.path) is False: + logging.warning("Missing step_data.json file. INIT empty file") with open(self.path, "w") as file: json.dump(dict(basic={}, steps=[]), file, indent=4, sort_keys=True) #load from json file - with open(self.path) as json_file: - data = json.load(json_file) - self.basic_data = data["basic"] - self.profile = data["steps"] + try: + with open(self.path) as json_file: + data = json.load(json_file) + self.basic_data = data["basic"] + self.profile = data["steps"] + # Start step after start up + self.profile = list(map(lambda item: self.create(item), self.profile)) + if startActive is True: + active_step = self.find_by_status("A") + if active_step is not None: + asyncio.get_event_loop().create_task(self.start_step(active_step)) + #self._loop.create_task(self.start_step(active_step)) + except: + logging.warning("Invalid step_data.json file - Creating empty file") + os.remove(self.path) + with open(self.path, "w") as file: + json.dump(dict(basic={"name": ""}, steps=[]), file, indent=4, sort_keys=True) + + with open(self.path) as json_file: + data = json.load(json_file) + self.basic_data = data["basic"] + self.profile = data["steps"] - # Start step after start up - self.profile = list(map(lambda item: self.create(item), self.profile)) - if startActive is True: - active_step = self.find_by_status("A") - if active_step is not None: - asyncio.get_event_loop().create_task(self.start_step(active_step)) - #self._loop.create_task(self.start_step(active_step)) + # Start step after start up + self.profile = list(map(lambda item: self.create(item), self.profile)) + if startActive is True: + active_step = self.find_by_status("A") + if active_step is not None: + asyncio.get_event_loop().create_task(self.start_step(active_step)) + #self._loop.create_task(self.start_step(active_step)) async def add(self, item: Step): logging.debug("Add step") diff --git a/cbpi/extension/ConfigUpdate/__init__.py b/cbpi/extension/ConfigUpdate/__init__.py index 4b4de2a..ae6b42e 100644 --- a/cbpi/extension/ConfigUpdate/__init__.py +++ b/cbpi/extension/ConfigUpdate/__init__.py @@ -45,6 +45,7 @@ class ConfigUpdate(CBPiExtension): influxdbuser = self.cbpi.config.get("INFLUXDBUSER", None) influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None) influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None) + influxdbmeasurement = self.cbpi.config.get("INFLUXDBMEASUREMENT", 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) @@ -267,6 +268,14 @@ class ConfigUpdate(CBPiExtension): except: logger.warning('Unable to update config') + ## Check if influxdbname is in config + if influxdbmeasurement is None: + logger.info("INIT Influxdb measurementname") + try: + await self.cbpi.config.add("INFLUXDBMEASUREMENT", "measurement", ConfigType.STRING, "Name of the measurement in your INFLUXDB database (default: measurement)") + except: + logger.warning('Unable to update config') + if mqttupdate is None: logger.info("INIT MQTT update frequency for Kettles and Fermenters") try: diff --git a/cbpi/extension/FermenterHysteresis/__init__.py b/cbpi/extension/FermenterHysteresis/__init__.py index 0376d01..d100d62 100644 --- a/cbpi/extension/FermenterHysteresis/__init__.py +++ b/cbpi/extension/FermenterHysteresis/__init__.py @@ -44,8 +44,8 @@ class FermenterAutostart(CBPiExtension): @parameters([Property.Number(label="HeaterOffsetOn", configurable=True, description="Offset as decimal number when the heater is switched on. Should be greater then 'HeaterOffsetOff'. For example a value of 2 switches on the heater if the current temperature is 2 degrees below the target temperature"), Property.Number(label="HeaterOffsetOff", configurable=True, description="Offset as decimal number when the heater is switched off. Should be smaller then 'HeaterOffsetOn'. For example a value of 1 switches off the heater if the current temperature is 1 degree below the target temperature"), - Property.Number(label="CoolerOffsetOn", configurable=True, description="Offset as decimal number when the cooler is switched on. Should be greater then 'CoolerOffsetOff'. For example a value of 2 switches on the cooler if the current temperature is 2 degrees below the target temperature"), - Property.Number(label="CoolerOffsetOff", configurable=True, description="Offset as decimal number when the cooler is switched off. Should be smaller then 'CoolerOffsetOn'. For example a value of 1 switches off the cooler if the current temperature is 1 degree below the target temperature"), + Property.Number(label="CoolerOffsetOn", configurable=True, description="Offset as decimal number when the cooler is switched on. Should be greater then 'CoolerOffsetOff'. For example a value of 2 switches on the cooler if the current temperature is 2 degrees above the target temperature"), + Property.Number(label="CoolerOffsetOff", configurable=True, description="Offset as decimal number when the cooler is switched off. Should be smaller then 'CoolerOffsetOn'. For example a value of 1 switches off the cooler if the current temperature is 1 degree above the target temperature"), Property.Select(label="AutoStart", options=["Yes","No"],description="Autostart Fermenter on cbpi start"), Property.Sensor(label="sensor2",description="Optional Sensor for LCDisplay(e.g. iSpindle)")]) diff --git a/cbpi/extension/httpsensor/__init__.py b/cbpi/extension/httpsensor/__init__.py index 3b89c69..0abd2a4 100644 --- a/cbpi/extension/httpsensor/__init__.py +++ b/cbpi/extension/httpsensor/__init__.py @@ -2,19 +2,38 @@ import asyncio from aiohttp import web from cbpi.api import * - +import time +from datetime import datetime import re -import random - +import logging +from cbpi.api.dataclasses import NotificationAction, NotificationType cache = {} -@parameters([Property.Text(label="Key", configurable=True, description="Http Key")]) +@parameters([Property.Text(label="Key", configurable=True, description="Http Key"), + Property.Number(label="Timeout", configurable="True",unit="sec",description="Timeout in seconds to send notification (default:60 | deactivated: 0)") +]) class HTTPSensor(CBPiSensor): def __init__(self, cbpi, id, props): super(HTTPSensor, self).__init__(cbpi, id, props) self.running = True self.value = 0 + self.timeout=int(self.props.get("Timeout", 60)) + self.starttime = time.time() + self.notificationsend = False + self.nextchecktime=self.starttime+self.timeout + self.sensor=self.get_sensor(self.id) + self.lastdata=time.time() + + async def Confirm(self, **kwargs): + self.nextchecktime = time.time() + self.timeout + self.notificationsend = False + pass + + async def message(self): + target_timestring= datetime.fromtimestamp(self.lastdata) + self.cbpi.notify("HTTPSensor Timeout", "Sensor '" + str(self.sensor.name) + "' did not respond. Last data received: "+target_timestring.strftime("%D %H:%M"), NotificationType.WARNING, action=[NotificationAction("OK", self.Confirm)]) + pass async def run(self): ''' @@ -22,12 +41,22 @@ class HTTPSensor(CBPiSensor): In this example the code is executed every second ''' while self.running is True: + if self.timeout !=0: + currenttime=time.time() + if currenttime > self.nextchecktime and self.notificationsend == False: + await self.message() + self.notificationsend=True try: cache_value = cache.pop(self.props.get("Key"), None) if cache_value is not None: self.value = float(cache_value) self.push_update(self.value) + if self.timeout !=0: + self.nextchecktime = currenttime + self.timeout + self.notificationsend = False + self.lastdata=time.time() except Exception as e: + logging.error(e) pass await asyncio.sleep(1) diff --git a/cbpi/extension/mqtt_sensor/__init__.py b/cbpi/extension/mqtt_sensor/__init__.py index e2d6e48..c50a776 100644 --- a/cbpi/extension/mqtt_sensor/__init__.py +++ b/cbpi/extension/mqtt_sensor/__init__.py @@ -1,14 +1,18 @@ # -*- coding: utf-8 -*- import asyncio - +from cbpi.api.dataclasses import NotificationAction, NotificationType from cbpi.api import parameters, Property, CBPiSensor from cbpi.api import * import logging import json +import time +from datetime import datetime @parameters([Property.Text(label="Topic", configurable=True, description="MQTT Topic"), Property.Text(label="PayloadDictionary", configurable=True, default_value="", - description="Where to find msg in payload, leave blank for raw payload")]) + description="Where to find msg in payload, leave blank for raw payload"), + Property.Number(label="Timeout", configurable="True",unit="sec", + description="Timeout in seconds to send notification (default:60 | deactivated: 0)")]) class MQTTSensor(CBPiSensor): def __init__(self, cbpi, id, props): @@ -19,6 +23,22 @@ class MQTTSensor(CBPiSensor): self.payload_text = self.payload_text.split('.') self.mqtt_task = self.cbpi.satellite.subcribe(self.Topic, self.on_message) self.value: float = 999 + self.timeout=int(self.props.get("Timeout", 60)) + self.starttime = time.time() + self.notificationsend = False + self.nextchecktime=self.starttime+self.timeout + self.lastdata=time.time() + self.sensor=self.get_sensor(self.id) + + async def Confirm(self, **kwargs): + self.nextchecktime = time.time() + self.timeout + self.notificationsend = False + pass + + async def message(self): + target_timestring= datetime.fromtimestamp(self.lastdata) + self.cbpi.notify("MQTTSensor Timeout", "Sensor '" + str(self.sensor.name) + "' did not respond. Last data received: "+target_timestring.strftime("%D %H:%M"), NotificationType.WARNING, action=[NotificationAction("OK", self.Confirm)]) + pass async def on_message(self, message): val = json.loads(message) @@ -31,11 +51,19 @@ class MQTTSensor(CBPiSensor): self.value = float(val) self.log_data(self.value) self.push_update(self.value) + if self.timeout !=0: + self.nextchecktime = time.time() + self.timeout + self.notificationsend = False + self.lastdata=time.time() except Exception as e: logging.info("MQTT Sensor Error {}".format(e)) async def run(self): while self.running: + if self.timeout !=0: + if time.time() > self.nextchecktime and self.notificationsend == False: + await self.message() + self.notificationsend=True await asyncio.sleep(1) def get_state(self): diff --git a/cbpi/http_endpoints/http_actor.py b/cbpi/http_endpoints/http_actor.py index 7738a5a..d3bec9b 100644 --- a/cbpi/http_endpoints/http_actor.py +++ b/cbpi/http_endpoints/http_actor.py @@ -247,7 +247,7 @@ class ActorHttpEndpoints(): """ actor_id = request.match_info['id'] data = await request.json() - print(data) + #print(data) await self.controller.call_action(actor_id, data.get("action"), data.get("parameter")) return web.Response(status=204) \ No newline at end of file diff --git a/cbpi/http_endpoints/http_dashboard.py b/cbpi/http_endpoints/http_dashboard.py index f24fe0c..38073cf 100644 --- a/cbpi/http_endpoints/http_dashboard.py +++ b/cbpi/http_endpoints/http_dashboard.py @@ -69,7 +69,7 @@ class DashBoardHttpEndpoints: data = await request.json() dashboard_id = int(request.match_info['id']) await self.cbpi.dashboard.add_content(dashboard_id, data) - print("##### SAVE") + #print("##### SAVE") return web.Response(status=204) @request_mapping(path="/{id:\d+}/content", method="DELETE", auth_required=False) diff --git a/cbpi/http_endpoints/http_fermenterrecipe.py b/cbpi/http_endpoints/http_fermenterrecipe.py index e974e3e..e653d76 100644 --- a/cbpi/http_endpoints/http_fermenterrecipe.py +++ b/cbpi/http_endpoints/http_fermenterrecipe.py @@ -58,7 +58,7 @@ class FermenterRecipeHttpEndpoints(): description: successful operation """ data = await request.json() - print(data) + #print(data) return web.json_response(dict(id=await self.controller.create(data.get("name")))) @@ -90,7 +90,7 @@ class FermenterRecipeHttpEndpoints(): data = await request.json() name = request.match_info['name'] await self.controller.save(name, data) - print(data) + #print(data) return web.Response(status=204) @request_mapping(path="/{name}", method="DELETE", auth_required=False) diff --git a/cbpi/http_endpoints/http_log.py b/cbpi/http_endpoints/http_log.py index f452026..8183edb 100644 --- a/cbpi/http_endpoints/http_log.py +++ b/cbpi/http_endpoints/http_log.py @@ -189,7 +189,6 @@ class LogHttpEndpoints: description: successful operation. """ data = await request.json() - print(data) return web.json_response(await self.cbpi.log.get_data2(data), dumps=json_dumps) @@ -240,7 +239,7 @@ class LogHttpEndpoints: data = await request.json() result = await self.cbpi.log.get_data(data) - print("JSON") - print(json.dumps(result, cls=ComplexEncoder)) - print("JSON----") + #print("JSON") + #print(json.dumps(result, cls=ComplexEncoder)) + #print("JSON----") return web.json_response(result, dumps=json_dumps) diff --git a/cbpi/http_endpoints/http_notification.py b/cbpi/http_endpoints/http_notification.py index b61e6f2..b25c4e3 100644 --- a/cbpi/http_endpoints/http_notification.py +++ b/cbpi/http_endpoints/http_notification.py @@ -35,6 +35,6 @@ class NotificationHttpEndpoints: notification_id = request.match_info['id'] action_id = request.match_info['action_id'] - print(notification_id, action_id) + #print(notification_id, action_id) self.cbpi.notification.notify_callback(notification_id, action_id) return web.Response(status=204) \ No newline at end of file diff --git a/cbpi/http_endpoints/http_recipe.py b/cbpi/http_endpoints/http_recipe.py index 734149b..9b8a6cc 100644 --- a/cbpi/http_endpoints/http_recipe.py +++ b/cbpi/http_endpoints/http_recipe.py @@ -57,7 +57,7 @@ class RecipeHttpEndpoints(): description: successful operation """ data = await request.json() - print(data) + #print(data) return web.json_response(dict(id=await self.controller.create(data.get("name")))) @@ -89,7 +89,7 @@ class RecipeHttpEndpoints(): data = await request.json() name = request.match_info['name'] await self.controller.save(name, data) - print(data) + #print(data) return web.Response(status=204) @request_mapping(path="/{name}", method="DELETE", auth_required=False) diff --git a/cbpi/http_endpoints/http_sensor.py b/cbpi/http_endpoints/http_sensor.py index bea7c08..d47aa9a 100644 --- a/cbpi/http_endpoints/http_sensor.py +++ b/cbpi/http_endpoints/http_sensor.py @@ -224,7 +224,7 @@ class SensorHttpEndpoints(): """ sensor_id = request.match_info['id'] data = await request.json() - print(data) + #print(data) await self.controller.call_action(sensor_id, data.get("action"), data.get("parameter")) return web.Response(status=204) diff --git a/cbpi/http_endpoints/http_system.py b/cbpi/http_endpoints/http_system.py index 823ef6f..8de8af2 100644 --- a/cbpi/http_endpoints/http_system.py +++ b/cbpi/http_endpoints/http_system.py @@ -28,6 +28,12 @@ class SystemHttpEndpoints: "200": description: successful operation """ + plugin_list = await self.cbpi.plugin.load_plugin_list("cbpi4gui") + try: + version= plugin_list[0].get("Version", "not detected") + except: + version="not detected" + return web.json_response(data=dict( actor=self.cbpi.actor.get_state(), fermenter=self.cbpi.fermenter.get_state(), @@ -37,6 +43,7 @@ class SystemHttpEndpoints: fermentersteps=self.cbpi.fermenter.get_fermenter_steps(), config=self.cbpi.config.get_state(), version=__version__, + guiversion=version, codename=__codename__) , dumps=json_dumps) diff --git a/cbpi/utils/encoder.py b/cbpi/utils/encoder.py index 616d3a9..475ec4d 100644 --- a/cbpi/utils/encoder.py +++ b/cbpi/utils/encoder.py @@ -13,10 +13,10 @@ class ComplexEncoder(JSONEncoder): elif isinstance(obj, datetime.datetime): return obj.__str__() elif isinstance(obj, Timestamp): - print("TIMe") + #print("TIMe") return obj.__str__() else: - print(type(obj)) + #print(type(obj)) raise TypeError() except Exception as e: diff --git a/requirements.txt b/requirements.txt index 674e3bd..9bcb800 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,27 +1,27 @@ typing-extensions>=4 -aiohttp==3.8.1 +aiohttp==3.8.3 aiohttp-auth==0.1.1 aiohttp-route-decorator==0.1.4 aiohttp-security==0.4.0 -aiohttp-session==2.11.0 +aiohttp-session==2.12.0 aiohttp-swagger==1.0.16 -aiojobs==1.0.0 +aiojobs==1.1.0 aiosqlite==0.17.0 cryptography==36.0.1 -requests==2.27.1 -voluptuous==0.12.2 +requests==2.28.1 +voluptuous==0.13.1 pyfiglet==0.8.post1 -pandas==1.4.1 -shortuuid==1.0.8 -tabulate==0.8.9 -numpy==1.22.2 +pandas==1.5.3 +shortuuid==1.0.11 +tabulate==0.9.0 +numpy==1.24.1 cbpi4gui -click==8.0.4 +click==8.1.3 importlib_metadata==4.11.1 -asyncio-mqtt -psutil==5.9.0 +asyncio-mqtt==0.16.1 +psutil==5.9.4 zipp>=0.5 -PyInquirer==1.0.3 -colorama==0.4.4 +colorama==0.4.6 pytest-aiohttp coverage==6.3.1 +inquirer==3.1.1 diff --git a/setup.py b/setup.py index a356451..5e32bee 100644 --- a/setup.py +++ b/setup.py @@ -39,29 +39,29 @@ setup(name='cbpi4', long_description_content_type='text/markdown', install_requires=[ "typing-extensions>=4", - "aiohttp==3.8.1", + "aiohttp==3.8.3", "aiohttp-auth==0.1.1", "aiohttp-route-decorator==0.1.4", "aiohttp-security==0.4.0", - "aiohttp-session==2.11.0", + "aiohttp-session==2.12.0", "aiohttp-swagger==1.0.16", - "aiojobs==1.0.0 ", + "aiojobs==1.1.0 ", "aiosqlite==0.17.0", "cryptography==36.0.1", - "requests==2.27.1", - "voluptuous==0.12.2", + "requests==2.28.1", + "voluptuous==0.13.1", "pyfiglet==0.8.post1", - 'click==8.0.4', - 'shortuuid==1.0.8', - 'tabulate==0.8.9', - 'asyncio-mqtt', - 'PyInquirer==1.0.3', - 'colorama==0.4.4', - 'psutil==5.9.0', + 'click==8.1.3', + 'shortuuid==1.0.11', + 'tabulate==0.9.0', + 'asyncio-mqtt==0.16.1', + 'inquirer==3.1.1', + 'colorama==0.4.6', + 'psutil==5.9.4', 'cbpi4gui', 'importlib_metadata', - 'numpy==1.22.2', - 'pandas==1.4.1'] + ( + 'numpy==1.24.1', + 'pandas==1.5.3'] + ( ['RPi.GPIO==0.7.1'] if raspberrypi else [] ), dependency_links=[ diff --git a/tests/cbpi-test-config/config.json b/tests/cbpi-test-config/config.json index 56f98c5..d6ab711 100644 --- a/tests/cbpi-test-config/config.json +++ b/tests/cbpi-test-config/config.json @@ -6,20 +6,6 @@ "type": "string", "value": "John Doe" }, - "BREWERY_NAME": { - "description": "Brewery Name", - "name": "BREWERY_NAME", - "options": null, - "type": "string", - "value": "CraftBeerPi Brewery" - }, - "MASH_TUN": { - "description": "Default Mash Tun", - "name": "MASH_TUN", - "options": null, - "type": "kettle", - "value": "" - }, "AddMashInStep": { "description": "Add MashIn Step automatically if not defined in recipe", "name": "AddMashInStep", @@ -36,26 +22,206 @@ "type": "select", "value": "Yes" }, + "AutoMode": { + "description": "Use AutoMode in steps", + "name": "AutoMode", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "select", + "value": "Yes" + }, + "BREWERY_NAME": { + "description": "Brewery Name", + "name": "BREWERY_NAME", + "options": null, + "type": "string", + "value": "Some New Brewery Name" + }, + "BoilKettle": { + "description": "Define Kettle that is used for Boil, Whirlpool and Cooldown. If not selected, MASH_TUN will be used", + "name": "BoilKettle", + "options": null, + "type": "kettle", + "value": "" + }, + "CSVLOGFILES": { + "description": "Write sensor data to csv logfiles", + "name": "CSVLOGFILES", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "select", + "value": "Yes" + }, + "INFLUXDB": { + "description": "Write sensor data to influxdb", + "name": "INFLUXDB", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "select", + "value": "No" + }, + "INFLUXDBADDR": { + "description": "IP Address of your influxdb server (If INFLUXDBCLOUD set to Yes use URL Address of your influxdb cloud server)", + "name": "INFLUXDBADDR", + "options": null, + "type": "string", + "value": "localhost" + }, + "INFLUXDBCLOUD": { + "description": "Write sensor data to influxdb cloud (INFLUXDB must set to Yes)", + "name": "INFLUXDBCLOUD", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "select", + "value": "No" + }, + "INFLUXDBNAME": { + "description": "Name of your influxdb database name (If INFLUXDBCLOUD set to Yes use bucket of your influxdb cloud database)", + "name": "INFLUXDBNAME", + "options": null, + "type": "string", + "value": "cbpi4" + }, + "INFLUXDBPORT": { + "description": "Port of your influxdb server", + "name": "INFLUXDBPORT", + "options": null, + "type": "string", + "value": "8086" + }, + "INFLUXDBPWD": { + "description": "Password for your influxdb database (only if required)(If INFLUXDBCLOUD set to Yes use token of your influxdb cloud database)", + "name": "INFLUXDBPWD", + "options": null, + "type": "string", + "value": " " + }, + "INFLUXDBUSER": { + "description": "User name for your influxdb database (only if required)(If INFLUXDBCLOUD set to Yes use organisation of your influxdb cloud database)", + "name": "INFLUXDBUSER", + "options": null, + "type": "string", + "value": " " + }, + "MASH_TUN": { + "description": "Default Mash Tun", + "name": "MASH_TUN", + "options": null, + "type": "kettle", + "value": "" + }, + "MQTTUpdate": { + "description": "Forced MQTT Update frequency in s for Kettle and Fermenter (no changes in payload required). Restart required after change", + "name": "MQTTUpdate", + "options": [ + { + "label": "30", + "value": 30 + }, + { + "label": "60", + "value": 60 + }, + { + "label": "120", + "value": 120 + }, + { + "label": "300", + "value": 300 + }, + { + "label": "Never", + "value": 0 + } + ], + "type": "select", + "value": 0 + }, + "NOTIFY_ON_ERROR": { + "description": "Send Notification on Logging Error", + "name": "NOTIFY_ON_ERROR", + "options": [ + { + "label": "Yes", + "value": "Yes" + }, + { + "label": "No", + "value": "No" + } + ], + "type": "select", + "value": "No" + }, + "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: empty", + "description": "API path to creation plugin. Default: upload . CHANGE ONLY IF USING A RECIPE CREATION PLUGIN", "name": "RECIPE_CREATION_PATH", "options": null, "type": "string", - "value": "" + "value": "upload" }, - "brewfather_api_key": { - "description": "Brewfather API Kay", - "name": "brewfather_api_key", + "SENSOR_LOG_BACKUP_COUNT": { + "description": "Max. number of backup logs", + "name": "SENSOR_LOG_BACKUP_COUNT", "options": null, - "type": "string", - "value": "" + "type": "number", + "value": 3 }, - "brewfather_user_id": { - "description": "Brewfather User ID", - "name": "brewfather_user_id", + "SENSOR_LOG_MAX_BYTES": { + "description": "Max. number of bytes in sensor logs", + "name": "SENSOR_LOG_MAX_BYTES", "options": null, - "type": "string", - "value": "" + "type": "number", + "value": 100000 }, "TEMP_UNIT": { "description": "Temperature Unit", @@ -73,9 +239,78 @@ "type": "select", "value": "C" }, - "AutoMode": { - "description": "Use AutoMode in steps", - "name": "AutoMode", + "brewfather_api_key": { + "description": "Brewfather API Key", + "name": "brewfather_api_key", + "options": null, + "type": "string", + "value": "" + }, + "brewfather_user_id": { + "description": "Brewfather User ID", + "name": "brewfather_user_id", + "options": null, + "type": "string", + "value": "" + }, + "current_dashboard_number": { + "description": "Number of current Dashboard", + "name": "current_dashboard_number", + "options": null, + "type": "number", + "value": 1 + }, + "max_dashboard_number": { + "description": "Max Number of Dashboards", + "name": "max_dashboard_number", + "options": [ + { + "label": "1", + "value": 1 + }, + { + "label": "2", + "value": 2 + }, + { + "label": "3", + "value": 3 + }, + { + "label": "4", + "value": 4 + }, + { + "label": "5", + "value": 5 + }, + { + "label": "6", + "value": 6 + }, + { + "label": "7", + "value": 7 + }, + { + "label": "8", + "value": 8 + }, + { + "label": "9", + "value": 9 + }, + { + "label": "10", + "value": 10 + } + ], + "type": "select", + "value": 4 + }, + "slow_pipe_animation": { + "description": "Slow down dashboard pipe animation taking up close to 100% of the CPU's capacity", + "name": "slow_pipe_animation", "options": [ { "label": "Yes", @@ -110,6 +345,13 @@ "type": "step", "value": "CooldownStep" }, + "steps_cooldown_actor": { + "description": "Actor to trigger cooldown water on and off (default: None)", + "name": "steps_cooldown_actor", + "options": null, + "type": "actor", + "value": "" + }, "steps_cooldown_sensor": { "description": "Alternative Sensor to monitor temperature durring cooldown (if not selected, Kettle Sensor will be used)", "name": "steps_cooldown_sensor", @@ -122,7 +364,7 @@ "name": "steps_cooldown_temp", "options": null, "type": "number", - "value": "20" + "value": 35 }, "steps_mash": { "description": "Mash step type", @@ -145,4 +387,4 @@ "type": "step", "value": "NotificationStep" } -} +} \ No newline at end of file diff --git a/tests/cbpi-test-config/fermenter_data.json b/tests/cbpi-test-config/fermenter_data.json index f788313..ce96464 100644 --- a/tests/cbpi-test-config/fermenter_data.json +++ b/tests/cbpi-test-config/fermenter_data.json @@ -1,5 +1,3 @@ { - "data": [ - - ] + "data": [] } \ No newline at end of file diff --git a/tests/cbpi-test-config/kettle.json b/tests/cbpi-test-config/kettle.json index f788313..ce96464 100644 --- a/tests/cbpi-test-config/kettle.json +++ b/tests/cbpi-test-config/kettle.json @@ -1,5 +1,3 @@ { - "data": [ - - ] + "data": [] } \ No newline at end of file diff --git a/tests/cbpi-test-config/sensor.json b/tests/cbpi-test-config/sensor.json index 6346c5f..ce96464 100644 --- a/tests/cbpi-test-config/sensor.json +++ b/tests/cbpi-test-config/sensor.json @@ -1,5 +1,3 @@ { - "data": [ - - ] + "data": [] } \ No newline at end of file diff --git a/tests/cbpi-test-config/step_data.json b/tests/cbpi-test-config/step_data.json index 60a4e28..fa93cc6 100644 --- a/tests/cbpi-test-config/step_data.json +++ b/tests/cbpi-test-config/step_data.json @@ -2,7 +2,5 @@ "basic": { "name": "" }, - "steps": [ - - ] + "steps": [] } \ No newline at end of file diff --git a/tests/test_actor.py b/tests/test_actor.py index 69ecc82..4e40b93 100644 --- a/tests/test_actor.py +++ b/tests/test_actor.py @@ -8,7 +8,6 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %( class ActorTestCase(CraftBeerPiTestCase): - @unittest_run_loop async def test_actor_switch(self): resp = await self.client.post(path="/login", data={"username": "cbpi", "password": "123"}) @@ -25,7 +24,6 @@ class ActorTestCase(CraftBeerPiTestCase): i = self.cbpi.actor.find_by_id("3CUJte4bkxDMFCtLX8eqsX") assert i.instance.state is False - @unittest_run_loop async def test_crud(self): data = { "name": "SomeActor", @@ -63,7 +61,6 @@ class ActorTestCase(CraftBeerPiTestCase): resp = await self.client.delete(path="/actor/%s" % sensor_id) assert resp.status == 204 - @unittest_run_loop async def test_crud_negative(self): data = { "name": "CustomActor", @@ -81,7 +78,6 @@ class ActorTestCase(CraftBeerPiTestCase): resp = await self.client.put(path="/actor/%s" % 9999, json=data) assert resp.status == 500 - @unittest_run_loop async def test_actor_action(self): resp = await self.client.post(path="/actor/1/action", json=dict(name="myAction", parameter=dict(name="Manuel"))) assert resp.status == 204 diff --git a/tests/test_config.py b/tests/test_config.py index ffe7cd0..c80280f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -5,19 +5,16 @@ from tests.cbpi_config_fixture import CraftBeerPiTestCase class ConfigTestCase(CraftBeerPiTestCase): - @unittest_run_loop async def test_get(self): assert self.cbpi.config.get("steps_boil_temp", 1) == "99" - @unittest_run_loop async def test_set_get(self): value = 35 await self.cbpi.config.set("steps_cooldown_temp", value) assert self.cbpi.config.get("steps_cooldown_temp", 1) == value - @unittest_run_loop async def test_http_set(self): value = "Some New Brewery Name" key = "BREWERY_NAME" @@ -27,12 +24,10 @@ class ConfigTestCase(CraftBeerPiTestCase): assert self.cbpi.config.get(key, -1) == value - @unittest_run_loop async def test_http_get(self): resp = await self.client.request("GET", "/config/") assert resp.status == 200 - @unittest_run_loop async def test_get_default(self): value = self.cbpi.config.get("HELLO_WORLD", "DefaultValue") assert value == "DefaultValue" \ No newline at end of file diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index ea7bbee..00f02e8 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -6,7 +6,6 @@ from cbpi.craftbeerpi import CraftBeerPi class DashboardTestCase(CraftBeerPiTestCase): - @unittest_run_loop async def test_crud(self): data = { "name": "MyDashboard", diff --git a/tests/test_index.py b/tests/test_index.py index c548113..e61656f 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -4,7 +4,6 @@ from tests.cbpi_config_fixture import CraftBeerPiTestCase class IndexTestCase(CraftBeerPiTestCase): - @unittest_run_loop async def test_index(self): @@ -12,19 +11,16 @@ class IndexTestCase(CraftBeerPiTestCase): resp = await self.client.get(path="/") assert resp.status == 200 - @unittest_run_loop async def test_404(self): # Test Index Page resp = await self.client.get(path="/abc") assert resp.status == 500 - @unittest_run_loop async def test_wrong_login(self): resp = await self.client.post(path="/login", data={"username": "beer", "password": "123"}) print("REPONSE STATUS", resp.status) assert resp.status == 403 - @unittest_run_loop async def test_login(self): resp = await self.client.post(path="/login", data={"username": "cbpi", "password": "123"}) diff --git a/tests/test_kettle.py b/tests/test_kettle.py index b481d6a..a130dd6 100644 --- a/tests/test_kettle.py +++ b/tests/test_kettle.py @@ -4,15 +4,13 @@ from tests.cbpi_config_fixture import CraftBeerPiTestCase class KettleTestCase(CraftBeerPiTestCase): - @unittest_run_loop async def test_get(self): resp = await self.client.request("GET", "/kettle") assert resp.status == 200 - kettle = resp.json() + kettle = await resp.json() assert kettle != None - @unittest_run_loop async def test_crud(self): data = { "name": "Test", diff --git a/tests/test_logger.py b/tests/test_logger.py index 2f84642..50d729b 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -7,7 +7,6 @@ import os class LoggerTestCase(CraftBeerPiTestCase): - @unittest_run_loop async def test_log_data(self): os.makedirs(os.path.join(".", "tests", "logs"), exist_ok=True) diff --git a/tests/test_notification_controller.py b/tests/test_notification_controller.py index 9de65ff..b1bb149 100644 --- a/tests/test_notification_controller.py +++ b/tests/test_notification_controller.py @@ -4,6 +4,5 @@ from tests.cbpi_config_fixture import CraftBeerPiTestCase class NotificationTestCase(CraftBeerPiTestCase): - @unittest_run_loop async def test_actor_switch(self): self.cbpi.notify("test", "test") \ No newline at end of file