Merge pull request #86 from avollkopf/development

Merge from Development Branch
This commit is contained in:
Alexander Vollkopf 2023-02-01 22:48:54 +01:00 committed by GitHub
commit a819782e5a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 548 additions and 215 deletions

View file

@ -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 \ RUN apt-get update \
&& apt-get upgrade -y && apt-get upgrade -y

View file

@ -3,7 +3,7 @@ RUN apk --no-cache add curl && mkdir /downloads
# Download installation files # Download installation files
RUN curl https://github.com/avollkopf/craftbeerpi4-ui/archive/main.zip -L -o ./downloads/cbpi-ui.zip 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 # Install dependencies
RUN apt-get update \ RUN apt-get update \

View file

@ -1,3 +1,3 @@
__version__ = "4.0.7" __version__ = "4.1.0"
__codename__ = "November Rain" __codename__ = "Groundhog Day"

View file

@ -15,7 +15,7 @@ from colorama import Fore, Back, Style
import importlib import importlib
from importlib_metadata import metadata from importlib_metadata import metadata
from tabulate import tabulate from tabulate import tabulate
from PyInquirer import prompt, print_json from inquirer import prompt
import platform import platform
import time import time

View file

@ -8,7 +8,7 @@ import shutil
import zipfile import zipfile
from pathlib import Path from pathlib import Path
import glob import glob
import json
class ConfigFolder: class ConfigFolder:
def __init__(self, configFolderPath, logsFolderPath): def __init__(self, configFolderPath, logsFolderPath):
@ -91,8 +91,8 @@ class ConfigFolder:
['actor.json', 'file'], ['actor.json', 'file'],
['sensor.json', 'file'], ['sensor.json', 'file'],
['kettle.json', 'file'], ['kettle.json', 'file'],
['fermenter_data.json', 'file'], #['fermenter_data.json', 'file'], created by fermentation_controller @ start if not available
['step_data.json', 'file'], #['step_data.json', 'file'], created by step_controller @ start if not available
['config.json', 'file'], ['config.json', 'file'],
['craftbeerpi.service', 'file'], ['craftbeerpi.service', 'file'],
['chromium.desktop', 'file'], ['chromium.desktop', 'file'],
@ -121,9 +121,23 @@ class ConfigFolder:
# if cbpi_dashboard_1.json doesnt exist at the new location (configFolderPath/dashboard) # 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. # 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. # 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')): 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): def inform_missing_content(self, whatsmissing : str):
if whatsmissing == "": if whatsmissing == "":

View file

@ -34,7 +34,7 @@ class DashboardController:
return {'elements': [], 'pathes': []} return {'elements': [], 'pathes': []}
async def add_content(self, dashboard_id, data): async def add_content(self, dashboard_id, data):
print(data) #print(data)
self.path = self.cbpi.config_folder.get_dashboard_path("cbpi_dashboard_" + str(dashboard_id)+ ".json") self.path = self.cbpi.config_folder.get_dashboard_path("cbpi_dashboard_" + str(dashboard_id)+ ".json")
with open(self.path, 'w') as outfile: with open(self.path, 'w') as outfile:
json.dump(data, outfile, indent=4, sort_keys=True) json.dump(data, outfile, indent=4, sort_keys=True)

View file

@ -36,7 +36,7 @@ class FermentationController:
def check_fermenter_file(self): def check_fermenter_file(self):
if os.path.exists(self.cbpi.config_folder.get_file_path("fermenter_data.json")) is False: 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 = {
"data": [ "data": [
] ]
@ -71,11 +71,23 @@ class FermentationController:
async def load(self): async def load(self):
with open(self.path) as json_file: try:
data = json.load(json_file) 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"]: for i in data["data"]:
self.data.append(self._create(i)) self.data.append(self._create(i))
def _create_step(self, fermenter, item): def _create_step(self, fermenter, item):
id = item.get("id") id = item.get("id")

View file

@ -53,6 +53,7 @@ class LogController:
self.influxdbname = self.cbpi.config.get("INFLUXDBNAME", None) self.influxdbname = self.cbpi.config.get("INFLUXDBNAME", None)
self.influxdbuser = self.cbpi.config.get("INFLUXDBUSER", None) self.influxdbuser = self.cbpi.config.get("INFLUXDBUSER", None)
self.influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None) self.influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None)
self.influxdbmeasurement = self.cbpi.config.get("INFLUXDBMEASUREMENT", "measurement")
id = name id = name
try: try:
@ -62,7 +63,7 @@ class LogController:
itemname=sensor.name.replace(" ", "_") itemname=sensor.name.replace(" ", "_")
for char in chars: for char in chars:
itemname = itemname.replace(char,chars[char]) 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: except Exception as e:
logging.error("InfluxDB ID Error: {}".format(e)) logging.error("InfluxDB ID Error: {}".format(e))
@ -153,15 +154,12 @@ class LogController:
return data return data
async def get_data2(self, ids) -> dict: 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() result = dict()
for id in ids: for id in ids:
# df = pd.read_csv("./logs/sensor_%s.log" % id, parse_dates=True, date_parser=dateparse, index_col='DateTime', names=['DateTime',"Values"], header=None)
# concat all logs
all_filenames = glob.glob(os.path.join(self.logsFolderPath,f"sensor_{id}.log*")) 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.resample('60s').max()
df = df.dropna() df = df.dropna()
result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()} result[id] = {"time": df.index.astype(str).tolist(), "value":df.Values.tolist()}
@ -180,12 +178,18 @@ class LogController:
def clear_log(self, name:str ) -> str: def clear_log(self, name:str ) -> str:
all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*")) all_filenames = glob.glob(os.path.join(self.logsFolderPath, f"sensor_{name}.log*"))
for f in all_filenames:
os.remove(f)
if name in self.datalogger: if name in self.datalogger:
self.datalogger[name].removeHandler(self.datalogger[name].handlers[0])
del self.datalogger[name] 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: def get_all_zip_file_names(self, name: str) -> list:

View file

@ -45,11 +45,11 @@ class NotificationController:
async def _call_listener(self, title, message, type, action): async def _call_listener(self, title, message, type, action):
for id, method in self.listener.items(): for id, method in self.listener.items():
print(id, method) #print(id, method)
asyncio.create_task(method(self.cbpi, title, message, type, action )) 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 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)) actions = list(map(lambda item: prepare_action(item), action))
self.callback_cache[notifcation_id] = 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)) 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) data = dict(type=type.value, title=title, message=message, action=actions, timeout=timeout)
self.cbpi.push_update(topic="cbpi/notification", data=data) self.cbpi.push_update(topic="cbpi/notification", data=data)
asyncio.create_task(self._call_listener(title, message, type, action)) asyncio.create_task(self._call_listener(title, message, type, action))

View file

@ -190,14 +190,14 @@ class PluginController():
return result return result
async def load_plugin_list(self): async def load_plugin_list(self, filter="cbpi"):
result = [] result = []
try: try:
discovered_plugins = { discovered_plugins = {
name: importlib.import_module(name) name: importlib.import_module(name)
for finder, name, ispkg for finder, name, ispkg
in pkgutil.iter_modules() 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(): for key, module in discovered_plugins.items():
from importlib.metadata import version from importlib.metadata import version

View file

@ -2,14 +2,16 @@
import asyncio import asyncio
import json import json
from re import M 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 contextlib import AsyncExitStack, asynccontextmanager
from cbpi import __version__ from cbpi import __version__
import logging import logging
import shortuuid
class SatelliteController: class SatelliteController:
def __init__(self, cbpi): def __init__(self, cbpi):
self.client_id = shortuuid.uuid()
self.cbpi = cbpi self.cbpi = cbpi
self.kettlecontroller = cbpi.kettle self.kettlecontroller = cbpi.kettle
self.fermentercontroller = cbpi.fermenter self.fermentercontroller = cbpi.fermenter
@ -34,7 +36,46 @@ class SatelliteController:
self.tasks = set() self.tasks = set()
async def init(self): 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): async def publish(self, topic, message, retain=False):
if self.client is not None and self.client._connected: if self.client is not None and self.client._connected:
@ -43,26 +84,25 @@ class SatelliteController:
except Exception as e: except Exception as e:
self.logger.warning("Failed to push data via mqtt: {}".format(e)) self.logger.warning("Failed to push data via mqtt: {}".format(e))
async def _actor_on(self, messages): async def _actor_on(self, message):
async for message in messages:
try: try:
topic_key = message.topic.split("/") topic_key = str(message.topic).split("/")
await self.cbpi.actor.on(topic_key[2]) await self.cbpi.actor.on(topic_key[2])
self.logger.warning("Processed actor {} on via mqtt".format(topic_key[2]))
except Exception as e: except Exception as e:
self.logger.warning("Failed to process actor on via mqtt: {}".format(e)) self.logger.warning("Failed to process actor on via mqtt: {}".format(e))
async def _actor_off(self, messages): async def _actor_off(self, message):
async for message in messages:
try: try:
topic_key = message.topic.split("/") topic_key = str(message.topic).split("/")
await self.cbpi.actor.off(topic_key[2]) await self.cbpi.actor.off(topic_key[2])
self.logger.warning("Processed actor {} off via mqtt".format(topic_key[2]))
except Exception as e: except Exception as e:
self.logger.warning("Failed to process actor off via mqtt: {}".format(e)) self.logger.warning("Failed to process actor off via mqtt: {}".format(e))
async def _actor_power(self, messages): async def _actor_power(self, message):
async for message in messages:
try: try:
topic_key = message.topic.split("/") topic_key = str(message.topic).split("/")
try: try:
power=int(message.payload.decode()) power=int(message.payload.decode())
if power > 100: if power > 100:
@ -76,8 +116,7 @@ class SatelliteController:
except: except:
self.logger.warning("Failed to set actor power via mqtt") self.logger.warning("Failed to set actor power via mqtt")
async def _kettleupdate(self, messages): async def _kettleupdate(self, message):
async for message in messages:
try: try:
self.kettle=self.kettlecontroller.get_state() self.kettle=self.kettlecontroller.get_state()
for item in self.kettle['data']: for item in self.kettle['data']:
@ -85,8 +124,7 @@ class SatelliteController:
except Exception as e: except Exception as e:
self.logger.warning("Failed to send kettleupdate via mqtt: {}".format(e)) self.logger.warning("Failed to send kettleupdate via mqtt: {}".format(e))
async def _fermenterupdate(self, messages): async def _fermenterupdate(self, message):
async for message in messages:
try: try:
self.fermenter=self.fermentercontroller.get_state() self.fermenter=self.fermentercontroller.get_state()
for item in self.fermenter['data']: for item in self.fermenter['data']:
@ -94,8 +132,7 @@ class SatelliteController:
except Exception as e: except Exception as e:
self.logger.warning("Failed to send fermenterupdate via mqtt: {}".format(e)) self.logger.warning("Failed to send fermenterupdate via mqtt: {}".format(e))
async def _actorupdate(self, messages): async def _actorupdate(self, message):
async for message in messages:
try: try:
self.actor=self.actorcontroller.get_state() self.actor=self.actorcontroller.get_state()
for item in self.actor['data']: for item in self.actor['data']:
@ -103,8 +140,7 @@ class SatelliteController:
except Exception as e: except Exception as e:
self.logger.warning("Failed to send actorupdate via mqtt: {}".format(e)) self.logger.warning("Failed to send actorupdate via mqtt: {}".format(e))
async def _sensorupdate(self, messages): async def _sensorupdate(self, message):
async for message in messages:
try: try:
self.sensor=self.sensorcontroller.get_state() self.sensor=self.sensorcontroller.get_state()
for item in self.sensor['data']: for item in self.sensor['data']:
@ -120,10 +156,11 @@ class SatelliteController:
while True: while True:
try: try:
if self.client._connected.done(): 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) await self.client.subscribe(topic)
async for message in messages: async for message in messages:
await method(message.payload.decode()) if message.topic.matches(topic):
await method(message.payload.decode())
except asyncio.CancelledError: except asyncio.CancelledError:
# Cancel # Cancel
self.logger.warning("Sub Cancelled") self.logger.warning("Sub Cancelled")
@ -134,45 +171,3 @@ class SatelliteController:
# wait before try to resubscribe # wait before try to resubscribe
await asyncio.sleep(5) 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)

View file

@ -6,6 +6,7 @@ import yaml
import logging import logging
import os.path import os.path
from os import listdir from os import listdir
import os
from os.path import isfile, join from os.path import isfile, join
import shortuuid import shortuuid
from cbpi.api.dataclasses import NotificationAction, Props, Step from cbpi.api.dataclasses import NotificationAction, Props, Step
@ -54,23 +55,42 @@ class StepController:
# create file if not exists # create file if not exists
if os.path.exists(self.path) is False: 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: with open(self.path, "w") as file:
json.dump(dict(basic={}, steps=[]), file, indent=4, sort_keys=True) json.dump(dict(basic={}, steps=[]), file, indent=4, sort_keys=True)
#load from json file #load from json file
with open(self.path) as json_file: try:
data = json.load(json_file) with open(self.path) as json_file:
self.basic_data = data["basic"] data = json.load(json_file)
self.profile = data["steps"] 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 # Start step after start up
self.profile = list(map(lambda item: self.create(item), self.profile)) self.profile = list(map(lambda item: self.create(item), self.profile))
if startActive is True: if startActive is True:
active_step = self.find_by_status("A") active_step = self.find_by_status("A")
if active_step is not None: if active_step is not None:
asyncio.get_event_loop().create_task(self.start_step(active_step)) asyncio.get_event_loop().create_task(self.start_step(active_step))
#self._loop.create_task(self.start_step(active_step)) #self._loop.create_task(self.start_step(active_step))
async def add(self, item: Step): async def add(self, item: Step):
logging.debug("Add step") logging.debug("Add step")

View file

@ -45,6 +45,7 @@ class ConfigUpdate(CBPiExtension):
influxdbuser = self.cbpi.config.get("INFLUXDBUSER", None) influxdbuser = self.cbpi.config.get("INFLUXDBUSER", None)
influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None) influxdbpwd = self.cbpi.config.get("INFLUXDBPWD", None)
influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None) influxdbcloud = self.cbpi.config.get("INFLUXDBCLOUD", None)
influxdbmeasurement = self.cbpi.config.get("INFLUXDBMEASUREMENT", None)
mqttupdate = self.cbpi.config.get("MQTTUpdate", None) mqttupdate = self.cbpi.config.get("MQTTUpdate", None)
PRESSURE_UNIT = self.cbpi.config.get("PRESSURE_UNIT", None) PRESSURE_UNIT = self.cbpi.config.get("PRESSURE_UNIT", None)
SENSOR_LOG_BACKUP_COUNT = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", None) SENSOR_LOG_BACKUP_COUNT = self.cbpi.config.get("SENSOR_LOG_BACKUP_COUNT", None)
@ -267,6 +268,14 @@ class ConfigUpdate(CBPiExtension):
except: except:
logger.warning('Unable to update config') 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: if mqttupdate is None:
logger.info("INIT MQTT update frequency for Kettles and Fermenters") logger.info("INIT MQTT update frequency for Kettles and Fermenters")
try: try:

View file

@ -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"), @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="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="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 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 above the target temperature"),
Property.Select(label="AutoStart", options=["Yes","No"],description="Autostart Fermenter on cbpi start"), 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)")]) Property.Sensor(label="sensor2",description="Optional Sensor for LCDisplay(e.g. iSpindle)")])

View file

@ -2,19 +2,38 @@
import asyncio import asyncio
from aiohttp import web from aiohttp import web
from cbpi.api import * from cbpi.api import *
import time
from datetime import datetime
import re import re
import random import logging
from cbpi.api.dataclasses import NotificationAction, NotificationType
cache = {} 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): class HTTPSensor(CBPiSensor):
def __init__(self, cbpi, id, props): def __init__(self, cbpi, id, props):
super(HTTPSensor, self).__init__(cbpi, id, props) super(HTTPSensor, self).__init__(cbpi, id, props)
self.running = True self.running = True
self.value = 0 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): async def run(self):
''' '''
@ -22,12 +41,22 @@ class HTTPSensor(CBPiSensor):
In this example the code is executed every second In this example the code is executed every second
''' '''
while self.running is True: 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: try:
cache_value = cache.pop(self.props.get("Key"), None) cache_value = cache.pop(self.props.get("Key"), None)
if cache_value is not None: if cache_value is not None:
self.value = float(cache_value) self.value = float(cache_value)
self.push_update(self.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: except Exception as e:
logging.error(e)
pass pass
await asyncio.sleep(1) await asyncio.sleep(1)

View file

@ -1,14 +1,18 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import asyncio import asyncio
from cbpi.api.dataclasses import NotificationAction, NotificationType
from cbpi.api import parameters, Property, CBPiSensor from cbpi.api import parameters, Property, CBPiSensor
from cbpi.api import * from cbpi.api import *
import logging import logging
import json import json
import time
from datetime import datetime
@parameters([Property.Text(label="Topic", configurable=True, description="MQTT Topic"), @parameters([Property.Text(label="Topic", configurable=True, description="MQTT Topic"),
Property.Text(label="PayloadDictionary", configurable=True, default_value="", 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): class MQTTSensor(CBPiSensor):
def __init__(self, cbpi, id, props): def __init__(self, cbpi, id, props):
@ -19,6 +23,22 @@ class MQTTSensor(CBPiSensor):
self.payload_text = self.payload_text.split('.') self.payload_text = self.payload_text.split('.')
self.mqtt_task = self.cbpi.satellite.subcribe(self.Topic, self.on_message) self.mqtt_task = self.cbpi.satellite.subcribe(self.Topic, self.on_message)
self.value: float = 999 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): async def on_message(self, message):
val = json.loads(message) val = json.loads(message)
@ -31,11 +51,19 @@ class MQTTSensor(CBPiSensor):
self.value = float(val) self.value = float(val)
self.log_data(self.value) self.log_data(self.value)
self.push_update(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: except Exception as e:
logging.info("MQTT Sensor Error {}".format(e)) logging.info("MQTT Sensor Error {}".format(e))
async def run(self): async def run(self):
while self.running: 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) await asyncio.sleep(1)
def get_state(self): def get_state(self):

View file

@ -247,7 +247,7 @@ class ActorHttpEndpoints():
""" """
actor_id = request.match_info['id'] actor_id = request.match_info['id']
data = await request.json() data = await request.json()
print(data) #print(data)
await self.controller.call_action(actor_id, data.get("action"), data.get("parameter")) await self.controller.call_action(actor_id, data.get("action"), data.get("parameter"))
return web.Response(status=204) return web.Response(status=204)

View file

@ -69,7 +69,7 @@ class DashBoardHttpEndpoints:
data = await request.json() data = await request.json()
dashboard_id = int(request.match_info['id']) dashboard_id = int(request.match_info['id'])
await self.cbpi.dashboard.add_content(dashboard_id, data) await self.cbpi.dashboard.add_content(dashboard_id, data)
print("##### SAVE") #print("##### SAVE")
return web.Response(status=204) return web.Response(status=204)
@request_mapping(path="/{id:\d+}/content", method="DELETE", auth_required=False) @request_mapping(path="/{id:\d+}/content", method="DELETE", auth_required=False)

View file

@ -58,7 +58,7 @@ class FermenterRecipeHttpEndpoints():
description: successful operation description: successful operation
""" """
data = await request.json() data = await request.json()
print(data) #print(data)
return web.json_response(dict(id=await self.controller.create(data.get("name")))) return web.json_response(dict(id=await self.controller.create(data.get("name"))))
@ -90,7 +90,7 @@ class FermenterRecipeHttpEndpoints():
data = await request.json() data = await request.json()
name = request.match_info['name'] name = request.match_info['name']
await self.controller.save(name, data) await self.controller.save(name, data)
print(data) #print(data)
return web.Response(status=204) return web.Response(status=204)
@request_mapping(path="/{name}", method="DELETE", auth_required=False) @request_mapping(path="/{name}", method="DELETE", auth_required=False)

View file

@ -189,7 +189,6 @@ class LogHttpEndpoints:
description: successful operation. description: successful operation.
""" """
data = await request.json() data = await request.json()
print(data)
return web.json_response(await self.cbpi.log.get_data2(data), dumps=json_dumps) return web.json_response(await self.cbpi.log.get_data2(data), dumps=json_dumps)
@ -240,7 +239,7 @@ class LogHttpEndpoints:
data = await request.json() data = await request.json()
result = await self.cbpi.log.get_data(data) result = await self.cbpi.log.get_data(data)
print("JSON") #print("JSON")
print(json.dumps(result, cls=ComplexEncoder)) #print(json.dumps(result, cls=ComplexEncoder))
print("JSON----") #print("JSON----")
return web.json_response(result, dumps=json_dumps) return web.json_response(result, dumps=json_dumps)

View file

@ -35,6 +35,6 @@ class NotificationHttpEndpoints:
notification_id = request.match_info['id'] notification_id = request.match_info['id']
action_id = request.match_info['action_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) self.cbpi.notification.notify_callback(notification_id, action_id)
return web.Response(status=204) return web.Response(status=204)

View file

@ -57,7 +57,7 @@ class RecipeHttpEndpoints():
description: successful operation description: successful operation
""" """
data = await request.json() data = await request.json()
print(data) #print(data)
return web.json_response(dict(id=await self.controller.create(data.get("name")))) return web.json_response(dict(id=await self.controller.create(data.get("name"))))
@ -89,7 +89,7 @@ class RecipeHttpEndpoints():
data = await request.json() data = await request.json()
name = request.match_info['name'] name = request.match_info['name']
await self.controller.save(name, data) await self.controller.save(name, data)
print(data) #print(data)
return web.Response(status=204) return web.Response(status=204)
@request_mapping(path="/{name}", method="DELETE", auth_required=False) @request_mapping(path="/{name}", method="DELETE", auth_required=False)

View file

@ -224,7 +224,7 @@ class SensorHttpEndpoints():
""" """
sensor_id = request.match_info['id'] sensor_id = request.match_info['id']
data = await request.json() data = await request.json()
print(data) #print(data)
await self.controller.call_action(sensor_id, data.get("action"), data.get("parameter")) await self.controller.call_action(sensor_id, data.get("action"), data.get("parameter"))
return web.Response(status=204) return web.Response(status=204)

View file

@ -28,6 +28,12 @@ class SystemHttpEndpoints:
"200": "200":
description: successful operation 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( return web.json_response(data=dict(
actor=self.cbpi.actor.get_state(), actor=self.cbpi.actor.get_state(),
fermenter=self.cbpi.fermenter.get_state(), fermenter=self.cbpi.fermenter.get_state(),
@ -37,6 +43,7 @@ class SystemHttpEndpoints:
fermentersteps=self.cbpi.fermenter.get_fermenter_steps(), fermentersteps=self.cbpi.fermenter.get_fermenter_steps(),
config=self.cbpi.config.get_state(), config=self.cbpi.config.get_state(),
version=__version__, version=__version__,
guiversion=version,
codename=__codename__) codename=__codename__)
, dumps=json_dumps) , dumps=json_dumps)

View file

@ -13,10 +13,10 @@ class ComplexEncoder(JSONEncoder):
elif isinstance(obj, datetime.datetime): elif isinstance(obj, datetime.datetime):
return obj.__str__() return obj.__str__()
elif isinstance(obj, Timestamp): elif isinstance(obj, Timestamp):
print("TIMe") #print("TIMe")
return obj.__str__() return obj.__str__()
else: else:
print(type(obj)) #print(type(obj))
raise TypeError() raise TypeError()
except Exception as e: except Exception as e:

View file

@ -1,27 +1,27 @@
typing-extensions>=4 typing-extensions>=4
aiohttp==3.8.1 aiohttp==3.8.3
aiohttp-auth==0.1.1 aiohttp-auth==0.1.1
aiohttp-route-decorator==0.1.4 aiohttp-route-decorator==0.1.4
aiohttp-security==0.4.0 aiohttp-security==0.4.0
aiohttp-session==2.11.0 aiohttp-session==2.12.0
aiohttp-swagger==1.0.16 aiohttp-swagger==1.0.16
aiojobs==1.0.0 aiojobs==1.1.0
aiosqlite==0.17.0 aiosqlite==0.17.0
cryptography==36.0.1 cryptography==36.0.1
requests==2.27.1 requests==2.28.1
voluptuous==0.12.2 voluptuous==0.13.1
pyfiglet==0.8.post1 pyfiglet==0.8.post1
pandas==1.4.1 pandas==1.5.3
shortuuid==1.0.8 shortuuid==1.0.11
tabulate==0.8.9 tabulate==0.9.0
numpy==1.22.2 numpy==1.24.1
cbpi4gui cbpi4gui
click==8.0.4 click==8.1.3
importlib_metadata==4.11.1 importlib_metadata==4.11.1
asyncio-mqtt asyncio-mqtt==0.16.1
psutil==5.9.0 psutil==5.9.4
zipp>=0.5 zipp>=0.5
PyInquirer==1.0.3 colorama==0.4.6
colorama==0.4.4
pytest-aiohttp pytest-aiohttp
coverage==6.3.1 coverage==6.3.1
inquirer==3.1.1

View file

@ -39,29 +39,29 @@ setup(name='cbpi4',
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
install_requires=[ install_requires=[
"typing-extensions>=4", "typing-extensions>=4",
"aiohttp==3.8.1", "aiohttp==3.8.3",
"aiohttp-auth==0.1.1", "aiohttp-auth==0.1.1",
"aiohttp-route-decorator==0.1.4", "aiohttp-route-decorator==0.1.4",
"aiohttp-security==0.4.0", "aiohttp-security==0.4.0",
"aiohttp-session==2.11.0", "aiohttp-session==2.12.0",
"aiohttp-swagger==1.0.16", "aiohttp-swagger==1.0.16",
"aiojobs==1.0.0 ", "aiojobs==1.1.0 ",
"aiosqlite==0.17.0", "aiosqlite==0.17.0",
"cryptography==36.0.1", "cryptography==36.0.1",
"requests==2.27.1", "requests==2.28.1",
"voluptuous==0.12.2", "voluptuous==0.13.1",
"pyfiglet==0.8.post1", "pyfiglet==0.8.post1",
'click==8.0.4', 'click==8.1.3',
'shortuuid==1.0.8', 'shortuuid==1.0.11',
'tabulate==0.8.9', 'tabulate==0.9.0',
'asyncio-mqtt', 'asyncio-mqtt==0.16.1',
'PyInquirer==1.0.3', 'inquirer==3.1.1',
'colorama==0.4.4', 'colorama==0.4.6',
'psutil==5.9.0', 'psutil==5.9.4',
'cbpi4gui', 'cbpi4gui',
'importlib_metadata', 'importlib_metadata',
'numpy==1.22.2', 'numpy==1.24.1',
'pandas==1.4.1'] + ( 'pandas==1.5.3'] + (
['RPi.GPIO==0.7.1'] if raspberrypi else [] ), ['RPi.GPIO==0.7.1'] if raspberrypi else [] ),
dependency_links=[ dependency_links=[

View file

@ -6,20 +6,6 @@
"type": "string", "type": "string",
"value": "John Doe" "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": { "AddMashInStep": {
"description": "Add MashIn Step automatically if not defined in recipe", "description": "Add MashIn Step automatically if not defined in recipe",
"name": "AddMashInStep", "name": "AddMashInStep",
@ -36,26 +22,206 @@
"type": "select", "type": "select",
"value": "Yes" "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": { "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", "name": "RECIPE_CREATION_PATH",
"options": null, "options": null,
"type": "string", "type": "string",
"value": "" "value": "upload"
}, },
"brewfather_api_key": { "SENSOR_LOG_BACKUP_COUNT": {
"description": "Brewfather API Kay", "description": "Max. number of backup logs",
"name": "brewfather_api_key", "name": "SENSOR_LOG_BACKUP_COUNT",
"options": null, "options": null,
"type": "string", "type": "number",
"value": "" "value": 3
}, },
"brewfather_user_id": { "SENSOR_LOG_MAX_BYTES": {
"description": "Brewfather User ID", "description": "Max. number of bytes in sensor logs",
"name": "brewfather_user_id", "name": "SENSOR_LOG_MAX_BYTES",
"options": null, "options": null,
"type": "string", "type": "number",
"value": "" "value": 100000
}, },
"TEMP_UNIT": { "TEMP_UNIT": {
"description": "Temperature Unit", "description": "Temperature Unit",
@ -73,9 +239,78 @@
"type": "select", "type": "select",
"value": "C" "value": "C"
}, },
"AutoMode": { "brewfather_api_key": {
"description": "Use AutoMode in steps", "description": "Brewfather API Key",
"name": "AutoMode", "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": [ "options": [
{ {
"label": "Yes", "label": "Yes",
@ -110,6 +345,13 @@
"type": "step", "type": "step",
"value": "CooldownStep" "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": { "steps_cooldown_sensor": {
"description": "Alternative Sensor to monitor temperature durring cooldown (if not selected, Kettle Sensor will be used)", "description": "Alternative Sensor to monitor temperature durring cooldown (if not selected, Kettle Sensor will be used)",
"name": "steps_cooldown_sensor", "name": "steps_cooldown_sensor",
@ -122,7 +364,7 @@
"name": "steps_cooldown_temp", "name": "steps_cooldown_temp",
"options": null, "options": null,
"type": "number", "type": "number",
"value": "20" "value": 35
}, },
"steps_mash": { "steps_mash": {
"description": "Mash step type", "description": "Mash step type",
@ -145,4 +387,4 @@
"type": "step", "type": "step",
"value": "NotificationStep" "value": "NotificationStep"
} }
} }

View file

@ -1,5 +1,3 @@
{ {
"data": [ "data": []
]
} }

View file

@ -1,5 +1,3 @@
{ {
"data": [ "data": []
]
} }

View file

@ -1,5 +1,3 @@
{ {
"data": [ "data": []
]
} }

View file

@ -2,7 +2,5 @@
"basic": { "basic": {
"name": "" "name": ""
}, },
"steps": [ "steps": []
]
} }

View file

@ -8,7 +8,6 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
class ActorTestCase(CraftBeerPiTestCase): class ActorTestCase(CraftBeerPiTestCase):
@unittest_run_loop
async def test_actor_switch(self): async def test_actor_switch(self):
resp = await self.client.post(path="/login", data={"username": "cbpi", "password": "123"}) 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") i = self.cbpi.actor.find_by_id("3CUJte4bkxDMFCtLX8eqsX")
assert i.instance.state is False assert i.instance.state is False
@unittest_run_loop
async def test_crud(self): async def test_crud(self):
data = { data = {
"name": "SomeActor", "name": "SomeActor",
@ -63,7 +61,6 @@ class ActorTestCase(CraftBeerPiTestCase):
resp = await self.client.delete(path="/actor/%s" % sensor_id) resp = await self.client.delete(path="/actor/%s" % sensor_id)
assert resp.status == 204 assert resp.status == 204
@unittest_run_loop
async def test_crud_negative(self): async def test_crud_negative(self):
data = { data = {
"name": "CustomActor", "name": "CustomActor",
@ -81,7 +78,6 @@ class ActorTestCase(CraftBeerPiTestCase):
resp = await self.client.put(path="/actor/%s" % 9999, json=data) resp = await self.client.put(path="/actor/%s" % 9999, json=data)
assert resp.status == 500 assert resp.status == 500
@unittest_run_loop
async def test_actor_action(self): async def test_actor_action(self):
resp = await self.client.post(path="/actor/1/action", json=dict(name="myAction", parameter=dict(name="Manuel"))) resp = await self.client.post(path="/actor/1/action", json=dict(name="myAction", parameter=dict(name="Manuel")))
assert resp.status == 204 assert resp.status == 204

View file

@ -5,19 +5,16 @@ from tests.cbpi_config_fixture import CraftBeerPiTestCase
class ConfigTestCase(CraftBeerPiTestCase): class ConfigTestCase(CraftBeerPiTestCase):
@unittest_run_loop
async def test_get(self): async def test_get(self):
assert self.cbpi.config.get("steps_boil_temp", 1) == "99" assert self.cbpi.config.get("steps_boil_temp", 1) == "99"
@unittest_run_loop
async def test_set_get(self): async def test_set_get(self):
value = 35 value = 35
await self.cbpi.config.set("steps_cooldown_temp", value) await self.cbpi.config.set("steps_cooldown_temp", value)
assert self.cbpi.config.get("steps_cooldown_temp", 1) == value assert self.cbpi.config.get("steps_cooldown_temp", 1) == value
@unittest_run_loop
async def test_http_set(self): async def test_http_set(self):
value = "Some New Brewery Name" value = "Some New Brewery Name"
key = "BREWERY_NAME" key = "BREWERY_NAME"
@ -27,12 +24,10 @@ class ConfigTestCase(CraftBeerPiTestCase):
assert self.cbpi.config.get(key, -1) == value assert self.cbpi.config.get(key, -1) == value
@unittest_run_loop
async def test_http_get(self): async def test_http_get(self):
resp = await self.client.request("GET", "/config/") resp = await self.client.request("GET", "/config/")
assert resp.status == 200 assert resp.status == 200
@unittest_run_loop
async def test_get_default(self): async def test_get_default(self):
value = self.cbpi.config.get("HELLO_WORLD", "DefaultValue") value = self.cbpi.config.get("HELLO_WORLD", "DefaultValue")
assert value == "DefaultValue" assert value == "DefaultValue"

View file

@ -6,7 +6,6 @@ from cbpi.craftbeerpi import CraftBeerPi
class DashboardTestCase(CraftBeerPiTestCase): class DashboardTestCase(CraftBeerPiTestCase):
@unittest_run_loop
async def test_crud(self): async def test_crud(self):
data = { data = {
"name": "MyDashboard", "name": "MyDashboard",

View file

@ -4,7 +4,6 @@ from tests.cbpi_config_fixture import CraftBeerPiTestCase
class IndexTestCase(CraftBeerPiTestCase): class IndexTestCase(CraftBeerPiTestCase):
@unittest_run_loop
async def test_index(self): async def test_index(self):
@ -12,19 +11,16 @@ class IndexTestCase(CraftBeerPiTestCase):
resp = await self.client.get(path="/") resp = await self.client.get(path="/")
assert resp.status == 200 assert resp.status == 200
@unittest_run_loop
async def test_404(self): async def test_404(self):
# Test Index Page # Test Index Page
resp = await self.client.get(path="/abc") resp = await self.client.get(path="/abc")
assert resp.status == 500 assert resp.status == 500
@unittest_run_loop
async def test_wrong_login(self): async def test_wrong_login(self):
resp = await self.client.post(path="/login", data={"username": "beer", "password": "123"}) resp = await self.client.post(path="/login", data={"username": "beer", "password": "123"})
print("REPONSE STATUS", resp.status) print("REPONSE STATUS", resp.status)
assert resp.status == 403 assert resp.status == 403
@unittest_run_loop
async def test_login(self): async def test_login(self):
resp = await self.client.post(path="/login", data={"username": "cbpi", "password": "123"}) resp = await self.client.post(path="/login", data={"username": "cbpi", "password": "123"})

View file

@ -4,15 +4,13 @@ from tests.cbpi_config_fixture import CraftBeerPiTestCase
class KettleTestCase(CraftBeerPiTestCase): class KettleTestCase(CraftBeerPiTestCase):
@unittest_run_loop
async def test_get(self): async def test_get(self):
resp = await self.client.request("GET", "/kettle") resp = await self.client.request("GET", "/kettle")
assert resp.status == 200 assert resp.status == 200
kettle = resp.json() kettle = await resp.json()
assert kettle != None assert kettle != None
@unittest_run_loop
async def test_crud(self): async def test_crud(self):
data = { data = {
"name": "Test", "name": "Test",

View file

@ -7,7 +7,6 @@ import os
class LoggerTestCase(CraftBeerPiTestCase): class LoggerTestCase(CraftBeerPiTestCase):
@unittest_run_loop
async def test_log_data(self): async def test_log_data(self):
os.makedirs(os.path.join(".", "tests", "logs"), exist_ok=True) os.makedirs(os.path.join(".", "tests", "logs"), exist_ok=True)

View file

@ -4,6 +4,5 @@ from tests.cbpi_config_fixture import CraftBeerPiTestCase
class NotificationTestCase(CraftBeerPiTestCase): class NotificationTestCase(CraftBeerPiTestCase):
@unittest_run_loop
async def test_actor_switch(self): async def test_actor_switch(self):
self.cbpi.notify("test", "test") self.cbpi.notify("test", "test")