diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 2037269..70a060f 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1,3 +1,3 @@ -__version__ = "4.3.1" +__version__ = "4.3.2" __codename__ = "Winter Storm" diff --git a/cbpi/configFolder.py b/cbpi/configFolder.py index 198bf29..54cd4eb 100644 --- a/cbpi/configFolder.py +++ b/cbpi/configFolder.py @@ -107,16 +107,24 @@ class ConfigFolder: for checking in required_config_content: if self.inform_missing_content(self.check_for_file_or_folder(os.path.join(self.configFolderPath, checking[0]), checking[1])): # since there is no complete config we now check if the config folder may be completely empty to show hints: - if len(os.listdir(os.path.join(self.configFolderPath))) == 0 : - print("***************************************************") - print(f"the config folder '{self.configFolderPath}' seems to be completely empty") - print("you might want to run 'cbpi setup'.print") - print("but you could also place your zipped config backup named") - print("'restored_config.zip' inside the mentioned config folder for") - print("cbpi4 to automatically unpack it") - print("of course you can also place your config files manually") - print("***************************************************") - return False + try: + if len(os.listdir(os.path.join(self.configFolderPath))) == 0 : + print("***************************************************") + print(f"the config folder '{self.configFolderPath}' seems to be completely empty") + print("you might want to run 'cbpi setup'.print") + print("but you could also place your zipped config backup named") + print("'restored_config.zip' inside the mentioned config folder for") + print("cbpi4 to automatically unpack it") + print("of course you can also place your config files manually") + print("***************************************************") + return False + except: + print("***************************************************") + print("Cannot find config folder!") + print("Please navigate to path where you did run 'cbpi setup'.") + print("Or run 'cbpi setup' before starting the server.") + print("***************************************************") + return False # if cbpi_dashboard_1.json does'nt exist at the new location (configFolderPath/dashboard) # we move every cbpi_dashboard_n.json file from the old location (configFolderPath) there. @@ -145,8 +153,11 @@ class ConfigFolder: # Starting with cbpi 4.2.0, the craftbeerpi.service file will be created dynamically from the template file based on the user id. # Therefore, the service file is replaced with a template file in the config folder if whatsmissing.find("craftbeerpi.template"): - self.copyDefaultFileIfNotExists("craftbeerpi.template") - return False + try: + self.copyDefaultFileIfNotExists("craftbeerpi.template") + return False + except: + pass print("***************************************************") print(f"CraftBeerPi config content not found: {whatsmissing}") print("Please run 'cbpi setup' before starting the server ") diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index 9dbc6ba..512a528 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -524,6 +524,11 @@ class FermentationController: except Exception as e: self.logger.error(e) + + def remove_key(self,d, key): + r = dict(d) + del r[key] + return r def push_update(self, key="fermenterupdate"): @@ -531,7 +536,8 @@ class FermentationController: self.cbpi.ws.send(dict(topic=key, data=list(map(lambda item: item.to_dict(), self.data)))) for item in self.data: - self.cbpi.push_update("cbpi/{}/{}".format(self.update_key,item.id), item.to_dict()) + fermenters=self.remove_key(item.to_dict(),"steps") + self.cbpi.push_update("cbpi/{}/{}".format(self.update_key,item.id), fermenters) pass else: fermentersteps=self.get_fermenter_steps() @@ -539,9 +545,20 @@ class FermentationController: # send mqtt update for active femrentersteps for fermenter in fermentersteps: + active = False for step in fermenter['steps']: if step['status'] == 'A': - self.cbpi.push_update("cbpi/{}/{}/{}".format(key,fermenter['id'],step['id']), step) + active=True + active_step=step +# self.cbpi.push_update("cbpi/{}/{}/{}".format(key,fermenter['id'],step['id']), step) + #else: + # self.cbpi.push_update("cbpi/{}/{}".format(key,fermenter['id']), "") + if active: + self.cbpi.push_update("cbpi/{}/{}".format(key,fermenter['id']), active_step) + else: + self.cbpi.push_update("cbpi/{}/{}".format(key,fermenter['id']), "") + + async def call_action(self, id, action, parameter) -> None: logging.info("FermenterStep Controller - call Action {} {}".format(id, action)) diff --git a/cbpi/controller/satellite_controller.py b/cbpi/controller/satellite_controller.py index 7877666..7b8e545 100644 --- a/cbpi/controller/satellite_controller.py +++ b/cbpi/controller/satellite_controller.py @@ -34,6 +34,11 @@ class SatelliteController: ] self.tasks = set() + + def remove_key(self,d, key): + r = dict(d) + del r[key] + return r async def init(self): @@ -132,7 +137,8 @@ class SatelliteController: try: self.fermenter=self.fermentercontroller.get_state() for item in self.fermenter['data']: - self.cbpi.push_update("cbpi/{}/{}".format("fermenterupdate",item['id']), item) + item_new=self.remove_key(item,"steps") + self.cbpi.push_update("cbpi/{}/{}".format("fermenterupdate",item['id']), item_new) except Exception as e: self.logger.warning("Failed to send fermenterupdate via mqtt: {}".format(e)) diff --git a/cbpi/controller/step_controller.py b/cbpi/controller/step_controller.py index 651b3fb..8e45689 100644 --- a/cbpi/controller/step_controller.py +++ b/cbpi/controller/step_controller.py @@ -9,7 +9,7 @@ from os import listdir import os from os.path import isfile, join import shortuuid -from cbpi.api.dataclasses import NotificationAction, Props, Step +from cbpi.api.dataclasses import NotificationAction, NotificationType, Props, Step from tabulate import tabulate from ..api.step import StepMove, StepResult, StepState @@ -156,14 +156,14 @@ class StepController: logging.info("BREWING COMPLETE") async def previous(self): - logging.info("Trigger Next") + logging.info("Trigger Previous") async def next(self): logging.info("Trigger Next") - print("\n\n\n\n") - print(self.profile) - print("\n\n\n\n") + #print("\n\n\n\n") + #print(self.profile) + #print("\n\n\n\n") step = self.find_by_status(StepState.ACTIVE) if step is not None: if step.instance is not None: @@ -299,6 +299,7 @@ class StepController: await step.instance.start() step.status = StepState.ACTIVE except Exception as e: + self.cbpi.notify("Error", "Can't start step. Please check step in Mash Profile", NotificationType.ERROR) logging.error("Failed to start step %s" % step) async def save_basic(self, data): diff --git a/cbpi/controller/system_controller.py b/cbpi/controller/system_controller.py index d357d1b..ebf6f42 100644 --- a/cbpi/controller/system_controller.py +++ b/cbpi/controller/system_controller.py @@ -1,6 +1,7 @@ import logging import os import shutil +import pkgutil import psutil import pathlib import json @@ -12,6 +13,8 @@ from cbpi.api.config import ConfigType from cbpi.api import * import zipfile import socket +import importlib +from tabulate import tabulate class SystemController: @@ -41,6 +44,26 @@ class SystemController: dir_name = pathlib.Path(self.cbpi.config_folder.get_file_path('')) shutil.make_archive(output_filename, 'zip', dir_name) + async def plugins_list(self): + result = [] + discovered_plugins = { + name: importlib.import_module(name) + for finder, name, ispkg + in pkgutil.iter_modules() + if name.startswith('cbpi') and len(name) > 4 + } + for key, module in discovered_plugins.items(): + from importlib.metadata import version + try: + from importlib.metadata import (distribution, metadata, + version) + meta = metadata(key) + result.append(dict(Name=meta["Name"], Version=meta["Version"], Author=meta["Author"], Homepage=meta["Home-page"], Summary=meta["Summary"])) + + except Exception as e: + print(e) + return tabulate(result, headers="keys") + async def downloadlog(self, logtime): filename = "cbpi4.log" fullname = pathlib.Path(os.path.join(".",filename)) @@ -60,7 +83,12 @@ class SystemController: else: os.system('journalctl --since \"{} hours ago\" -u craftbeerpi.service --output cat > {}'.format(logtime, fullname)) - os.system('cbpi plugins > {}'.format(fullpluginname)) + plugins = await self.plugins_list() + + with open(fullpluginname, 'w') as f: + f.write(plugins) + + #os.system('echo "{}" >> {}'.format(plugins,fullpluginname)) try: actors = self.cbpi.actor.get_state() diff --git a/cbpi/craftbeerpi.py b/cbpi/craftbeerpi.py index 149b2c3..c11295f 100644 --- a/cbpi/craftbeerpi.py +++ b/cbpi/craftbeerpi.py @@ -1,6 +1,7 @@ import asyncio import sys +import socket try: from asyncio import set_event_loop_policy, WindowsSelectorEventLoopPolicy except ImportError: @@ -276,6 +277,22 @@ class CraftBeerPi: self.app.add_routes([web.get('/', http_index), web.static('/static', os.path.join(os.path.dirname(__file__), "static"), show_index=True)]) + + def testport(self, port=8000): + HOST = "localhost" + # Creates a new socket + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + # Try to connect to the given host and port + if sock.connect_ex((HOST, port)) == 0: + #print("Port " + str(port) + " is open") # Connected successfully + isrunning = True + else: + #print("Port " + str(port) + " is closed") # Failed to connect because port is in use (or bad host) + isrunning = False + # Close the connection + sock.close() + return isrunning async def init_serivces(self): @@ -304,4 +321,9 @@ class CraftBeerPi: return self.app def start(self): - web.run_app(self.init_serivces(), port=self.static_config.get("port", 2202)) + port=self.static_config.get("port",8000) + if not self.testport(port): + web.run_app(self.init_serivces(), port=port) + else: + logging.error("Port {} is already in use! Please check, if server is already running (e.g. in automode)".format(port)) + exit(1) \ No newline at end of file diff --git a/cbpi/extension/FermenterHysteresis/__init__.py b/cbpi/extension/FermenterHysteresis/__init__.py index d100d62..48c17c8 100644 --- a/cbpi/extension/FermenterHysteresis/__init__.py +++ b/cbpi/extension/FermenterHysteresis/__init__.py @@ -46,6 +46,8 @@ class FermenterAutostart(CBPiExtension): 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 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.Number(label="HeaterMaxPower", configurable=True,description="Max Power [%] for Heater (default: 100)"), + Property.Number(label="CoolerMaxPower", configurable=True ,description="Max Power [%] for Cooler (default: 100)"), 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)")]) @@ -57,7 +59,9 @@ class FermenterHysteresis(CBPiFermenterLogic): self.heater_offset_max = float(self.props.get("HeaterOffsetOff", 0)) self.cooler_offset_min = float(self.props.get("CoolerOffsetOn", 0)) self.cooler_offset_max = float(self.props.get("CoolerOffsetOff", 0)) - + self.heater_max_power = int(self.props.get("HeaterMaxPower", 100)) + self.cooler_max_power = int(self.props.get("CoolerMaxPower", 100)) + self.fermenter = self.get_fermenter(self.id) self.heater = self.fermenter.heater self.cooler = self.fermenter.cooler @@ -81,7 +85,7 @@ class FermenterHysteresis(CBPiFermenterLogic): if sensor_value + self.heater_offset_min <= target_temp: if self.heater and (heater_state == False): - await self.actor_on(self.heater) + await self.actor_on(self.heater, self.heater_max_power) if sensor_value + self.heater_offset_max >= target_temp: if self.heater and (heater_state == True): @@ -89,7 +93,7 @@ class FermenterHysteresis(CBPiFermenterLogic): if sensor_value >= self.cooler_offset_min + target_temp: if self.cooler and (cooler_state == False): - await self.actor_on(self.cooler) + await self.actor_on(self.cooler, self.cooler_max_power) if sensor_value <= self.cooler_offset_max + target_temp: if self.cooler and (cooler_state == True): diff --git a/cbpi/extension/mqtt_util/__init__.py b/cbpi/extension/mqtt_util/__init__.py index 3cce4c4..54e6102 100644 --- a/cbpi/extension/mqtt_util/__init__.py +++ b/cbpi/extension/mqtt_util/__init__.py @@ -37,6 +37,11 @@ class MQTTUtil(CBPiExtension): self.push_update() await asyncio.sleep(self.mqttupdate) + def remove_key(self,d, key): + r = dict(d) + del r[key] + return r + def push_update(self): # try: # self.actor=self.actorcontroller.get_state() @@ -61,8 +66,9 @@ class MQTTUtil(CBPiExtension): pass try: self.fermenter=self.fermentationcontroller.get_state() - for item in self.fermenter['data']: - self.cbpi.push_update("cbpi/{}/{}".format("fermenterupdate",item['id']), item) + for item in self.fermenter['data']: + item_new=self.remove_key(item,"steps") + self.cbpi.push_update("cbpi/{}/{}".format("fermenterupdate",item['id']), item_new) except Exception as e: logging.error(e) pass diff --git a/requirements.txt b/requirements.txt index f13b20c..09cd3d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ typing-extensions>=4 -aiohttp==3.9.1 +aiohttp==3.9.3 aiohttp-auth==0.1.1 aiohttp-route-decorator==0.1.4 aiohttp-security==0.5.0 diff --git a/setup.py b/setup.py index bf23d32..8def85b 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ setup(name='cbpi4', long_description_content_type='text/markdown', install_requires=[ "typing-extensions>=4", - "aiohttp==3.9.1", + "aiohttp==3.9.3", "aiohttp-auth==0.1.1", "aiohttp-route-decorator==0.1.4", "aiohttp-security==0.5.0", diff --git a/tests/cbpi-test-config/config.json b/tests/cbpi-test-config/config.json index c71bc3b..48da07d 100644 --- a/tests/cbpi-test-config/config.json +++ b/tests/cbpi-test-config/config.json @@ -80,7 +80,7 @@ "options": null, "source": "hidden", "type": "string", - "value": "4.2.0.a6" + "value": "4.3.2.a6" }, "CSVLOGFILES": { "description": "Write sensor data to csv logfiles (enabling requires restart)", @@ -326,6 +326,14 @@ "type": "number", "value": 1 }, + "current_grid": { + "description": "Dashboard Grid Width", + "name": "current_grid", + "options": null, + "source": "hidden", + "type": "number", + "value": 5 + }, "max_dashboard_number": { "description": "Max Number of Dashboards", "name": "max_dashboard_number",