From d7c1b64493f1ff45fbfb1ac29664cd3c2a921d5d Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 2 Jan 2022 11:25:56 +0100 Subject: [PATCH 1/9] Added Fermenters (development) Added fermenter type Added fermenter logic (incl. new class) -> will require cbpi4ui -> >= 0.1.a1 Still under development, but fermentation w/o steps should be working --- cbpi/__init__.py | 2 +- cbpi/api/__init__.py | 2 + cbpi/api/base.py | 9 + cbpi/api/config.py | 1 + cbpi/api/dataclasses.py | 22 +- cbpi/api/fermenter_logic.py | 51 ++++ cbpi/api/property.py | 17 ++ cbpi/cli.py | 5 + cbpi/config/fermenter_data.json | 5 + cbpi/controller/fermentation_controller.py | 144 +++++++-- cbpi/controller/plugin_controller.py | 6 +- cbpi/craftbeerpi.py | 9 +- cbpi/extension/ConfigUpdate/__init__.py | 3 +- .../extension/FermenterHysteresis/__init__.py | 115 +++++++ .../extension/FermenterHysteresis/config.yaml | 3 + cbpi/http_endpoints/http_fermentation.py | 285 ++++++++++++++++++ cbpi/http_endpoints/http_system.py | 1 + 17 files changed, 651 insertions(+), 29 deletions(-) create mode 100644 cbpi/api/fermenter_logic.py create mode 100644 cbpi/config/fermenter_data.json create mode 100644 cbpi/extension/FermenterHysteresis/__init__.py create mode 100644 cbpi/extension/FermenterHysteresis/config.yaml create mode 100644 cbpi/http_endpoints/http_fermentation.py diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 829fd1d..6f09c48 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.0.59" +__version__ = "4.0.1.a1" diff --git a/cbpi/api/__init__.py b/cbpi/api/__init__.py index c265b9a..c62cd1a 100644 --- a/cbpi/api/__init__.py +++ b/cbpi/api/__init__.py @@ -9,6 +9,7 @@ __all__ = ["CBPiActor", "parameters", "background_task", "CBPiKettleLogic", + "CBPiFermenterLogic", "CBPiException", "KettleException", "SensorException", @@ -22,5 +23,6 @@ from cbpi.api.extension import * from cbpi.api.property import * from cbpi.api.decorator import * from cbpi.api.kettle_logic import * +from cbpi.api.fermenter_logic import * from cbpi.api.step import * from cbpi.api.exceptions import * \ No newline at end of file diff --git a/cbpi/api/base.py b/cbpi/api/base.py index 844e645..e4c0c9a 100644 --- a/cbpi/api/base.py +++ b/cbpi/api/base.py @@ -29,6 +29,15 @@ class CBPiBase(metaclass=ABCMeta): async def set_target_temp(self,id, temp): await self.cbpi.kettle.set_target_temp(id, temp) + def get_fermenter(self,id): + return self.cbpi.fermenter._find_by_id(id) + + def get_fermenter_target_temp(self,id): + return self.cbpi.fermenter._find_by_id(id).target_temp + + async def set_fermenter_target_temp(self,id, temp): + await self.cbpi.fermenter.set_target_temp(id, temp) + def get_sensor(self,id): return self.cbpi.sensor.find_by_id(id) diff --git a/cbpi/api/config.py b/cbpi/api/config.py index b20aa4a..96157b3 100644 --- a/cbpi/api/config.py +++ b/cbpi/api/config.py @@ -8,5 +8,6 @@ class ConfigType(Enum): ACTOR = "actor" SENSOR = "sensor" STEP = "step" + FERMENTER = "fermenter" diff --git a/cbpi/api/dataclasses.py b/cbpi/api/dataclasses.py index 67edfa5..1524aa5 100644 --- a/cbpi/api/dataclasses.py +++ b/cbpi/api/dataclasses.py @@ -62,7 +62,7 @@ class Actor: def __str__(self): return "name={} props={}, state={}, type={}, power={}".format(self.name, self.props, self.state, self.type, self.power) def to_dict(self): - return dict(id=self.id, name=self.name, type=self.type, props=self.props.to_dict(), state2="HELLO WORLD", state=self.instance.get_state(), power=self.power) + return dict(id=self.id, name=self.name, type=self.type, props=self.props.to_dict(), state=self.instance.get_state(), power=self.power) @dataclass @@ -125,15 +125,30 @@ class Step: class Fermenter: id: str = None name: str = None + sensor: Sensor = None + heater: Actor = None + cooler: Actor = None brewname: str = None props: Props = Props() target_temp: int = 0 + type: str = None steps: List[Step]= field(default_factory=list) + instance: str = None + + def __str__(self): - return "id={} name={} brewname={} props={} temp={} steps={}".format(self.id, self.name, self.brewname, self.props, self.target_temp, self.steps) + return "id={} name={} sensor={} heater={} cooler={} brewname={} props={} temp={} type={} steps={}".format(self.id, self.name, self.sensor, self.heater, self.cooler, self.brewname, self.props, self.target_temp, self.type, self.steps) def to_dict(self): + + if self.instance is not None: + + state = self.instance.state + + else: + state = False + steps = list(map(lambda item: item.to_dict(), self.steps)) - return dict(id=self.id, name=self.name, target_temp=self.target_temp, steps=steps, props=self.props.to_dict() if self.props is not None else None) + return dict(id=self.id, name=self.name, state=state, sensor=self.sensor, heater=self.heater, cooler=self.cooler, brewname=self.brewname, props=self.props.to_dict() if self.props is not None else None, target_temp=self.target_temp, type=self.type, steps=steps) @dataclass @@ -162,6 +177,7 @@ class ConfigType(Enum): NUMBER="number" SELECT="select" STEP="step" + FERMENTER="fermenter" @dataclass class Config: diff --git a/cbpi/api/fermenter_logic.py b/cbpi/api/fermenter_logic.py new file mode 100644 index 0000000..fbebdf8 --- /dev/null +++ b/cbpi/api/fermenter_logic.py @@ -0,0 +1,51 @@ +from cbpi.api.base import CBPiBase +from cbpi.api.extension import CBPiExtension +from abc import ABCMeta +import logging +import asyncio + + + +class CBPiFermenterLogic(CBPiBase, metaclass=ABCMeta): + + def __init__(self, cbpi, id, props): + self.cbpi = cbpi + self.id = id + self.props = props + self.state = False + self.running = False + + def init(self): + pass + + async def on_start(self): + pass + + async def on_stop(self): + pass + + async def run(self): + pass + + async def _run(self): + + try: + await self.on_start() + self.cancel_reason = await self.run() + except asyncio.CancelledError as e: + pass + finally: + await self.on_stop() + + def get_state(self): + return dict(running=self.state) + + async def start(self): + + self.state = True + + async def stop(self): + + self.task.cancel() + await self.task + self.state = False diff --git a/cbpi/api/property.py b/cbpi/api/property.py index 048c989..354ca47 100644 --- a/cbpi/api/property.py +++ b/cbpi/api/property.py @@ -98,6 +98,23 @@ class Property(object): The user select a kettle which is available in the system. The value of this variable will be the kettle id ''' + def __init__(self, label, description=""): + ''' + + :param label: + :param description: + ''' + + PropertyType.__init__(self) + self.label = label + self.configurable = True + self.description = description + + class Fermenter(PropertyType): + ''' + The user select a fermenter which is available in the system. The value of this variable will be the fermenter id + ''' + def __init__(self, label, description=""): ''' diff --git a/cbpi/cli.py b/cbpi/cli.py index 89e3e5d..56a1887 100644 --- a/cbpi/cli.py +++ b/cbpi/cli.py @@ -42,6 +42,11 @@ def create_config_file(): destfile = os.path.join(".", 'config') shutil.copy(srcfile, destfile) + if os.path.exists(os.path.join(".", 'config', "fermenter_data.json")) is False: + srcfile = os.path.join(os.path.dirname(__file__), "config", "fermenter_data.json") + destfile = os.path.join(".", 'config') + shutil.copy(srcfile, destfile) + if os.path.exists(os.path.join(".", 'config', "step_data.json")) is False: srcfile = os.path.join(os.path.dirname(__file__), "config", "step_data.json") destfile = os.path.join(".", 'config') diff --git a/cbpi/config/fermenter_data.json b/cbpi/config/fermenter_data.json new file mode 100644 index 0000000..f788313 --- /dev/null +++ b/cbpi/config/fermenter_data.json @@ -0,0 +1,5 @@ +{ + "data": [ + + ] +} \ No newline at end of file diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index 057cdd2..8623552 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -1,4 +1,3 @@ - import asyncio import cbpi import copy @@ -9,19 +8,15 @@ from os import listdir from os.path import isfile, join import shortuuid from cbpi.api.dataclasses import Fermenter, FermenterStep, Props, Step +from cbpi.controller.basic_controller2 import BasicController from tabulate import tabulate import sys, os from ..api.step import CBPiStep, StepMove, StepResult, StepState -logging.basicConfig(format='%(asctime)s,%(msecs)d %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s', - datefmt='%Y-%m-%d:%H:%M:%S', - level=logging.INFO) - class FermentStep: - def __init__(self, cbpi, step, on_done) -> None: self.cbpi = cbpi self.logger = logging.getLogger(__name__) @@ -85,9 +80,10 @@ class FermentStep: async def on_stop(self): pass -class FermenationController: +class FermentationController: def __init__(self, cbpi): + self.update_key = "fermenterupdate" self.cbpi = cbpi self.logger = logging.getLogger(__name__) self.path = os.path.join(".", 'config', "fermenter_data.json") @@ -96,6 +92,27 @@ class FermenationController: self.types = {} self.cbpi.app.on_cleanup.append(self.shutdown) + async def init(self): + logging.info("INIT Fermentation Controller") + self.check_fermenter_file() + await self.load() + pass + + def check_fermenter_file(self): + if os.path.exists(os.path.join(".", 'config', "fermenter_data.json")) is False: + logging.info("INIT fermenter_data.json file") + data = { + "data": [ + ] + } + destfile = os.path.join(".", 'config', "fermenter_data.json") + json.dump(data,open(destfile,'w'),indent=4, sort_keys=True) + + def push_update(self): + self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) + self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) + pass + async def shutdown(self, app=None): self.save() for fermenter in self.data: @@ -114,7 +131,11 @@ class FermenationController: with open(self.path) as json_file: d = json.load(json_file) self.data = list(map(lambda item: self._create(item), d)) - + + #for item in self.data: + # logging.info("{} Starting ".format(item.name)) + # await self.start(item.id) + def _create_step(self, fermenter, item): id = item.get("id") name = item.get("name") @@ -140,20 +161,37 @@ class FermenationController: def _create(self, data): id = data.get("id") name = data.get("name") + sensor = data.get("sensor") + heater = data.get("heater") + cooler = data.get("cooler") + logictype = data.get("type") + temp = data.get("target_temp") brewname = data.get("brewname") props = Props(data.get("props", {})) - fermenter = Fermenter(id, name, brewname, props, 0) + fermenter = Fermenter(id, name, sensor, heater, cooler, brewname, props, temp, logictype) fermenter.steps = list(map(lambda item: self._create_step(fermenter, item), data.get("steps", []))) + self.push_update() + #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) + #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) + return fermenter def _find_by_id(self, id): return next((item for item in self.data if item.id == id), None) - async def init(self): - pass - async def get_all(self): - return self.data + return list(map(lambda x: x.to_dict(), self.data)) + + def get_types(self): +# logging.info("{} Get Types".format(self.name)) + result = {} + for key, value in self.types.items(): + result[key] = dict(name=value.get("name"), properties=value.get("properties"), actions=value.get("actions")) + return result + + def get_state(self): +# logging.info("{} Get State".format(self.name)) + return {"data": list(map(lambda x: x.to_dict(), self.data)), "types":self.get_types()} async def get(self, id: str ): return self._find_by_id(id) @@ -162,12 +200,21 @@ class FermenationController: data.id = shortuuid.uuid() self.data.append(data) self.save() + self.push_update() + #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) + #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) return data async def update(self, item: Fermenter ): + logging.info(item) + def _update(old_item: Fermenter, item: Fermenter): old_item.name = item.name + old_item.sensor = item.sensor + old_item.heater = item.heater + old_item.cooler = item.cooler + old_item.type = item.type old_item.brewname = item.brewname old_item.props = item.props old_item.target_temp = item.target_temp @@ -175,12 +222,31 @@ class FermenationController: self.data = list(map(lambda old: _update(old, item) if old.id == item.id else old, self.data)) self.save() + self.push_update() + #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) + #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) return item + async def set_target_temp(self, id: str, target_temp): + try: + item = self._find_by_id(id) + logging.info(item.target_temp) + if item: + item.target_temp = target_temp + self.save() + self.push_update() + #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) + #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) + except Exception as e: + logging.error("Failed to set Target Temp {} {}".format(id, e)) + async def delete(self, id: str ): item = self._find_by_id(id) self.data = list(filter(lambda item: item.id != id, self.data)) self.save() + self.push_update() + #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) + #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) def save(self): with open(self.path, "w") as file: @@ -191,7 +257,7 @@ class FermenationController: step.id = shortuuid.uuid() item = self._find_by_id(id) - step.instance = FermentStep( self.cbpi, step.id, step.name, None, self._done) + step.instance = FermentStep( self.cbpi, step, self._done) item.steps.append(step) self.save() @@ -223,10 +289,11 @@ class FermenationController: if step is None: self.logger.info("No futher step to start") + else: + await step.instance.start() + step.status = StepState.ACTIVE + self.save() - await step.instance.start() - step.status = StepState.ACTIVE - self.save() except Exception as e: self.logger.error(e) @@ -240,6 +307,46 @@ class FermenationController: except Exception as e: self.logger.error(e) + async def start_logic(self, id): + try: + item = self._find_by_id(id) + logging.info("{} Start Id {} ".format(item.name, id)) + if item.instance is not None and item.instance.running is True: + logging.warning("{} already running {}".format(item.name, id)) + return + if item.type is None: + logging.warning("{} No Type {}".format(item.name, id)) + return + clazz = self.types[item.type]["class"] + item.instance = clazz(self.cbpi, item.id, item.props) + + await item.instance.start() + item.instance.running = True + item.instance.task = self._loop.create_task(item.instance._run()) + + logging.info("{} started {}".format(item.name, id)) + +# await self.push_udpate() + except Exception as e: + logging.error("{} Cant start {} - {}".format(item.name, id, e)) + + async def toggle(self, id): + + try: + item = self._find_by_id(id) + #logging.info(item) + + if item.instance is None or item.instance.state == False: + await self.start_logic(id) + else: + await item.instance.stop() + self.push_update() + #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) + #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) + + except Exception as e: + logging.error("Failed to switch on FermenterLogic {} {}".format(id, e)) + async def next(self, id): self.logger.info("Next {} ".format(id)) @@ -283,5 +390,4 @@ class FermenationController: except Exception as e: self.logger.error(e) - - + \ No newline at end of file diff --git a/cbpi/controller/plugin_controller.py b/cbpi/controller/plugin_controller.py index b61b05c..193b905 100644 --- a/cbpi/controller/plugin_controller.py +++ b/cbpi/controller/plugin_controller.py @@ -31,8 +31,7 @@ class PluginController(): try: logger.info("Trying to load plugin %s" % filename) data = load_config(os.path.join( - this_directory, "../extension/%s/config.yaml" % filename)) - + this_directory, "../extension/%s/config.yaml" % filename)) if (data.get("active") is True and data.get("version") == 4): self.modules[filename] = import_module( "cbpi.extension.%s" % (filename)) @@ -75,6 +74,9 @@ class PluginController(): if issubclass(clazz, CBPiKettleLogic): self.cbpi.kettle.types[name] = self._parse_step_props(clazz, name) + if issubclass(clazz, CBPiFermenterLogic): + self.cbpi.fermenter.types[name] = self._parse_step_props(clazz, name) + if issubclass(clazz, CBPiSensor): self.cbpi.sensor.types[name] = self._parse_step_props(clazz, name) diff --git a/cbpi/craftbeerpi.py b/cbpi/craftbeerpi.py index e8a6067..7a5f685 100644 --- a/cbpi/craftbeerpi.py +++ b/cbpi/craftbeerpi.py @@ -26,6 +26,7 @@ from cbpi.controller.sensor_controller import SensorController from cbpi.controller.step_controller import StepController from cbpi.controller.recipe_controller import RecipeController from cbpi.controller.upload_controller import UploadController +from cbpi.controller.fermentation_controller import FermentationController from cbpi.controller.system_controller import SystemController from cbpi.controller.satellite_controller import SatelliteController @@ -49,6 +50,7 @@ from cbpi.http_endpoints.http_system import SystemHttpEndpoints from cbpi.http_endpoints.http_log import LogHttpEndpoints from cbpi.http_endpoints.http_notification import NotificationHttpEndpoints from cbpi.http_endpoints.http_upload import UploadHttpEndpoints +from cbpi.http_endpoints.http_fermentation import FermentationHttpEndpoints import shortuuid logger = logging.getLogger(__name__) @@ -115,8 +117,9 @@ class CraftBeerPi: self.satellite = None if str(self.static_config.get("mqtt", False)).lower() == "true": self.satellite: SatelliteController = SatelliteController(self) - self.dashboard = DashboardController(self) + self.fermenter : FermentationController = FermentationController(self) + self.http_step = StepHttpEndpoints(self) self.http_recipe = RecipeHttpEndpoints(self) self.http_sensor = SensorHttpEndpoints(self) @@ -129,7 +132,7 @@ class CraftBeerPi: self.http_log = LogHttpEndpoints(self) self.http_notification = NotificationHttpEndpoints(self) self.http_upload = UploadHttpEndpoints(self) - + self.http_fermenter = FermentationHttpEndpoints(self) self.login = Login(self) @@ -279,7 +282,7 @@ class CraftBeerPi: await self.kettle.init() await self.call_initializer(self.app) await self.dashboard.init() - + await self.fermenter.init() self._swagger_setup() diff --git a/cbpi/extension/ConfigUpdate/__init__.py b/cbpi/extension/ConfigUpdate/__init__.py index 7d7cc63..a6714f7 100644 --- a/cbpi/extension/ConfigUpdate/__init__.py +++ b/cbpi/extension/ConfigUpdate/__init__.py @@ -1,9 +1,10 @@ -import os, threading, time +import os, threading, time, shutil from aiohttp import web import logging from unittest.mock import MagicMock, patch import asyncio import random +import json from cbpi.api import * from cbpi.api.config import ConfigType from cbpi.api.base import CBPiBase diff --git a/cbpi/extension/FermenterHysteresis/__init__.py b/cbpi/extension/FermenterHysteresis/__init__.py new file mode 100644 index 0000000..d8e4ae9 --- /dev/null +++ b/cbpi/extension/FermenterHysteresis/__init__.py @@ -0,0 +1,115 @@ +import asyncio +from asyncio import tasks +import logging +from cbpi.api import * +import aiohttp +from aiohttp import web +from cbpi.controller.fermentation_controller import FermentationController +from cbpi.api.dataclasses import Fermenter, Props, Step +from cbpi.api.base import CBPiBase +from cbpi.api.config import ConfigType +import json +import webbrowser + +class FermenterAutostart(CBPiExtension): + + def __init__(self,cbpi): + self.cbpi = cbpi + self._task = asyncio.create_task(self.run()) + self.controller : FermentationController = cbpi.fermenter + + + async def run(self): + logging.info("Starting Fermenter Autorun") + #get all kettles + self.fermenter = self.controller.get_state() + for id in self.fermenter['data']: + try: + self.autostart=(id['props']['AutoStart']) + if self.autostart == "Yes": + fermenter_id=(id['id']) + logging.info("Enabling Autostart for Fermenter {}".format(fermenter_id)) + self.fermenter=self.cbpi.fermenter._find_by_id(fermenter_id) + try: + if (self.fermenter.instance is None or self.fermenter.instance.state == False): + await self.cbpi.fermenter.toggle(self.fermenter.id) + logging.info("Successfully switched on Ferenterlogic for Fermenter {}".format(self.fermenter.id)) + except Exception as e: + logging.error("Failed to switch on FermenterLogic {} {}".format(self.fermenter.id, e)) + except: + pass + + +@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.Select(label="AutoStart", options=["Yes","No"],description="Autostart Fermenter on cbpi start")]) + +class FermenterHysteresis(CBPiFermenterLogic): + + async def run(self): + try: + self.heater_offset_min = float(self.props.get("HeaterOffsetOn", 0)) + 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.fermenter = self.get_fermenter(self.id) + self.heater = self.fermenter.heater + self.cooler = self.fermenter.cooler + + target_temp = self.get_fermenter_target_temp(self.id) + if target_temp == 0: + await self.set_fermenter_target_temp(self.id,int(self.props.get("TargetTemp", 0))) + + + while self.running == True: + + sensor_value = self.get_sensor_value(self.fermenter.sensor).get("value") + target_temp = self.get_fermenter_target_temp(self.id) + + if sensor_value + self.heater_offset_min <= target_temp: + if self.heater: + await self.actor_on(self.heater) + + if sensor_value + self.heater_offset_max >= target_temp: + if self.heater: + await self.actor_off(self.heater) + + if sensor_value >= self.cooler_offset_min + target_temp: + if self.cooler: + await self.actor_on(self.cooler) + + if sensor_value <= self.cooler_offset_max + target_temp: + if self.cooler: + await self.actor_off(self.cooler) + + await asyncio.sleep(1) + + except asyncio.CancelledError as e: + pass + except Exception as e: + logging.error("CustomLogic Error {}".format(e)) + finally: + self.running = False + if self.heater: + await self.actor_off(self.heater) + if self.cooler: + await self.actor_off(self.cooler) + + + +def setup(cbpi): + + ''' + This method is called by the server during startup + Here you need to register your plugins at the server + + :param cbpi: the cbpi core + :return: + ''' + + cbpi.plugin.register("Fermenter Hysteresis", FermenterHysteresis) + cbpi.plugin.register("Fermenter Autostart", FermenterAutostart) + diff --git a/cbpi/extension/FermenterHysteresis/config.yaml b/cbpi/extension/FermenterHysteresis/config.yaml new file mode 100644 index 0000000..2fefeda --- /dev/null +++ b/cbpi/extension/FermenterHysteresis/config.yaml @@ -0,0 +1,3 @@ +name: FermenterHysteresis +version: 4 +active: true \ No newline at end of file diff --git a/cbpi/http_endpoints/http_fermentation.py b/cbpi/http_endpoints/http_fermentation.py new file mode 100644 index 0000000..ac523b4 --- /dev/null +++ b/cbpi/http_endpoints/http_fermentation.py @@ -0,0 +1,285 @@ +from cbpi.controller.fermentation_controller import FermentationController +from cbpi.api.dataclasses import Fermenter, Step, Props +from aiohttp import web +from cbpi.api import * +import logging +import json + +auth = False + +class FermentationHttpEndpoints(): + + def __init__(self, cbpi): + self.cbpi = cbpi + self.controller : FermentationController = cbpi.fermenter + self.cbpi.register(self, "/fermenter") + + @request_mapping(path="/", auth_required=False) + async def http_get_all(self, request): + """ + --- + description: Show all Fermenters + tags: + - Fermenter + responses: + "204": + description: successful operation + """ + data= self.controller.get_state() + return web.json_response(data=data) + + + @request_mapping(path="/", method="POST", auth_required=False) + async def http_add(self, request): + """ + --- + description: add one Fermenter + tags: + - Fermenter + parameters: + - in: body + name: body + description: Create a Fermenter + required: true + + schema: + type: object + + properties: + name: + type: string + sensor: + type: "integer" + format: "int64" + heater: + type: "integer" + format: "int64" + cooler: + type: "integer" + format: "int64" + target_temp: + type: "integer" + format: "int64" + type: + type: string + props: + type: object + example: + name: "Fermenter 1" + type: "CustomFermenterLogic" + sensor: "FermenterSensor" + heater: "FermenterHeater" + cooler: "FermenterCooler" + props: {} + + responses: + "204": + description: successful operation + """ + data = await request.json() + fermenter = Fermenter(id=id, name=data.get("name"), sensor=data.get("sensor"), heater=data.get("heater"), cooler=data.get("cooler"), brewname=data.get("brewname"), target_temp=data.get("target_temp"), props=Props(data.get("props", {})), type=data.get("type")) + response_data = await self.controller.create(fermenter) + return web.json_response(data=response_data.to_dict()) + + + @request_mapping(path="/{id}", method="PUT", auth_required=False) + async def http_update(self, request): + """ + --- + description: Update a Fermenter (NOT YET IMPLEMENTED) + tags: + - Fermenter + parameters: + - name: "id" + in: "path" + description: "Fermenter ID" + required: true + type: "integer" + format: "int64" + - in: body + name: body + description: Update a Fermenter + required: false + schema: + type: object + properties: + name: + type: string + type: + type: string + config: + props: object + responses: + "200": + description: successful operation + """ + id = request.match_info['id'] + data = await request.json() + fermenter = Fermenter(id=id, name=data.get("name"), sensor=data.get("sensor"), heater=data.get("heater"), cooler=data.get("cooler"), brewname=data.get("brewname"), target_temp=data.get("target_temp"), props=Props(data.get("props", {})), type=data.get("type")) + return web.json_response(data=(await self.controller.update(fermenter)).to_dict()) + + @request_mapping(path="/{id}", method="DELETE", auth_required=False) + async def http_delete_one(self, request): + """ + --- + description: Delete an actor + tags: + - Fermenter + parameters: + - name: "id" + in: "path" + description: "Fermenter ID" + required: true + type: "string" + responses: + "204": + description: successful operation + """ + id = request.match_info['id'] + await self.controller.delete(id) + return web.Response(status=204) + +# @request_mapping(path="/{id}/on", method="POST", auth_required=False) +# async def http_on(self, request) -> web.Response: + """ + + --- + description: Switch actor on + tags: + - Fermenter + parameters: + - name: "id" + in: "path" + description: "Actor ID" + required: true + type: "string" + + responses: + "204": + description: successful operation + "405": + description: invalid HTTP Met + """ +# id = request.match_info['id'] +# await self.controller.start(id) +# return web.Response(status=204) + +# @request_mapping(path="/{id}/off", method="POST", auth_required=False) +# async def http_off(self, request) -> web.Response: + """ + + --- + description: Switch actor on + tags: + - Fermenter + + parameters: + - name: "id" + in: "path" + description: "Actor ID" + required: true + type: "string" + + responses: + "204": + description: successful operation + "405": + description: invalid HTTP Met + """ +# id = request.match_info['id'] +# await self.controller.off(id) +# return web.Response(status=204) + + @request_mapping(path="/{id}/toggle", method="POST", auth_required=False) + async def http_toggle(self, request) -> web.Response: + """ + + --- + description: Switch actor on + tags: + - Fermenter + + parameters: + - name: "id" + in: "path" + description: "Kettle ID" + required: true + type: "string" + + responses: + "204": + description: successful operation + "405": + description: invalid HTTP Met + """ + id = request.match_info['id'] + await self.controller.toggle(id) + return web.Response(status=204) + +# @request_mapping(path="/{id}/action", method="POST", auth_required=auth) +# async def http_action(self, request) -> web.Response: + """ + + --- + description: Toogle an actor on or off + tags: + - Fermenter + parameters: + - name: "id" + in: "path" + description: "Actor ID" + required: true + type: "integer" + format: "int64" + - in: body + name: body + description: Update an actor + required: false + schema: + type: object + properties: + name: + type: string + parameter: + type: object + responses: + "204": + description: successful operation + """ +# actor_id = request.match_info['id'] +# data = await request.json() +# await self.controller.call_action(actor_id, data.get("name"), data.get("parameter")) + + return web.Response(status=204) + @request_mapping(path="/{id}/target_temp", method="POST", auth_required=auth) + async def http_target(self, request) -> web.Response: + """ + + --- + description: Set Target Temp for Fermenter + tags: + - Fermenter + parameters: + - name: "id" + in: "path" + description: "Fermenter ID" + required: true + type: "integer" + format: "int64" + - in: body + name: body + description: Update Temp + required: true + schema: + type: object + properties: + temp: + type: integer + responses: + "204": + description: successful operation + """ + id = request.match_info['id'] + data = await request.json() + await self.controller.set_target_temp(id,data.get("temp")) + return web.Response(status=204) diff --git a/cbpi/http_endpoints/http_system.py b/cbpi/http_endpoints/http_system.py index aa3121b..629077d 100644 --- a/cbpi/http_endpoints/http_system.py +++ b/cbpi/http_endpoints/http_system.py @@ -29,6 +29,7 @@ class SystemHttpEndpoints: """ return web.json_response(data=dict( actor=self.cbpi.actor.get_state(), + fermenter=self.cbpi.fermenter.get_state(), sensor=self.cbpi.sensor.get_state(), kettle=self.cbpi.kettle.get_state(), step=self.cbpi.step.get_state(), From 751b3dcad9fbc40da26b8a59ae07c434a7c9af3b Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 2 Jan 2022 12:43:33 +0100 Subject: [PATCH 2/9] Bug fix for first startup w/o fermenter --- cbpi/__init__.py | 2 +- cbpi/controller/fermentation_controller.py | 56 ++++++++++--------- .../extension/FermenterHysteresis/__init__.py | 36 ++++++------ cbpi/http_endpoints/http_system.py | 6 ++ 4 files changed, 56 insertions(+), 44 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 6f09c48..b8b91bd 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.1.a1" +__version__ = "4.0.1.a2" diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index 8623552..4edcd5e 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -88,7 +88,7 @@ class FermentationController: self.logger = logging.getLogger(__name__) self.path = os.path.join(".", 'config', "fermenter_data.json") self._loop = asyncio.get_event_loop() - self.data = {} + self.data = [] self.types = {} self.cbpi.app.on_cleanup.append(self.shutdown) @@ -125,16 +125,14 @@ class FermentationController: self.logger.error(e) async def load(self): - if os.path.exists(self.path) is False: - with open(self.path, "w") as file: - json.dump(dict(basic={}, steps=[]), file, indent=4, sort_keys=True) +# if os.path.exists(self.path) is False: +# with open(self.path, "w") as file: +# json.dump(dict(basic={}, steps=[]), file, indent=4, sort_keys=True) with open(self.path) as json_file: - d = json.load(json_file) - self.data = list(map(lambda item: self._create(item), d)) + data = json.load(json_file) - #for item in self.data: - # logging.info("{} Starting ".format(item.name)) - # await self.start(item.id) + for i in data["data"]: + self.data.append(self._create(i)) def _create_step(self, fermenter, item): id = item.get("id") @@ -159,22 +157,25 @@ class FermentationController: asyncio.create_task(self.start(step_instance.step.fermenter.id)) def _create(self, data): - id = data.get("id") - name = data.get("name") - sensor = data.get("sensor") - heater = data.get("heater") - cooler = data.get("cooler") - logictype = data.get("type") - temp = data.get("target_temp") - brewname = data.get("brewname") - props = Props(data.get("props", {})) - fermenter = Fermenter(id, name, sensor, heater, cooler, brewname, props, temp, logictype) - fermenter.steps = list(map(lambda item: self._create_step(fermenter, item), data.get("steps", []))) - self.push_update() - #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) - #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) - - return fermenter + try: + id = data.get("id") + name = data.get("name") + sensor = data.get("sensor") + heater = data.get("heater") + cooler = data.get("cooler") + logictype = data.get("type") + temp = data.get("target_temp") + brewname = data.get("brewname") + props = Props(data.get("props", {})) + fermenter = Fermenter(id, name, sensor, heater, cooler, brewname, props, temp, logictype) + fermenter.steps = list(map(lambda item: self._create_step(fermenter, item), data.get("steps", []))) + self.push_update() + #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) + #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) + return fermenter + except: + return + def _find_by_id(self, id): return next((item for item in self.data if item.id == id), None) @@ -191,6 +192,9 @@ class FermentationController: def get_state(self): # logging.info("{} Get State".format(self.name)) + if self.data == []: + logging.info(self.data) + return {"data": list(map(lambda x: x.to_dict(), self.data)), "types":self.get_types()} async def get(self, id: str ): @@ -390,4 +394,4 @@ class FermentationController: except Exception as e: self.logger.error(e) - \ No newline at end of file + diff --git a/cbpi/extension/FermenterHysteresis/__init__.py b/cbpi/extension/FermenterHysteresis/__init__.py index d8e4ae9..3f3200d 100644 --- a/cbpi/extension/FermenterHysteresis/__init__.py +++ b/cbpi/extension/FermenterHysteresis/__init__.py @@ -18,26 +18,28 @@ class FermenterAutostart(CBPiExtension): self._task = asyncio.create_task(self.run()) self.controller : FermentationController = cbpi.fermenter - async def run(self): logging.info("Starting Fermenter Autorun") #get all kettles - self.fermenter = self.controller.get_state() - for id in self.fermenter['data']: - try: - self.autostart=(id['props']['AutoStart']) - if self.autostart == "Yes": - fermenter_id=(id['id']) - logging.info("Enabling Autostart for Fermenter {}".format(fermenter_id)) - self.fermenter=self.cbpi.fermenter._find_by_id(fermenter_id) - try: - if (self.fermenter.instance is None or self.fermenter.instance.state == False): - await self.cbpi.fermenter.toggle(self.fermenter.id) - logging.info("Successfully switched on Ferenterlogic for Fermenter {}".format(self.fermenter.id)) - except Exception as e: - logging.error("Failed to switch on FermenterLogic {} {}".format(self.fermenter.id, e)) - except: - pass + try: + self.fermenter = self.controller.get_state() + for id in self.fermenter['data']: + try: + self.autostart=(id['props']['AutoStart']) + if self.autostart == "Yes": + fermenter_id=(id['id']) + logging.info("Enabling Autostart for Fermenter {}".format(fermenter_id)) + self.fermenter=self.cbpi.fermenter._find_by_id(fermenter_id) + try: + if (self.fermenter.instance is None or self.fermenter.instance.state == False): + await self.cbpi.fermenter.toggle(self.fermenter.id) + logging.info("Successfully switched on Ferenterlogic for Fermenter {}".format(self.fermenter.id)) + except Exception as e: + logging.error("Failed to switch on FermenterLogic {} {}".format(self.fermenter.id, e)) + except: + pass + except: + pass @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"), diff --git a/cbpi/http_endpoints/http_system.py b/cbpi/http_endpoints/http_system.py index 629077d..dddfcdd 100644 --- a/cbpi/http_endpoints/http_system.py +++ b/cbpi/http_endpoints/http_system.py @@ -27,6 +27,12 @@ class SystemHttpEndpoints: "200": description: successful operation """ + try: + fermenter=self.cbpi.fermenter.get_state() + logging.info(fermenter) + except: + logging.info("!!!!!!!!!!!!!!!!!!!!!!!!!Error get fermenter state!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + return web.json_response(data=dict( actor=self.cbpi.actor.get_state(), fermenter=self.cbpi.fermenter.get_state(), From ba42027e75ce2e0277e5f4864efbf72f73f6f707 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 2 Jan 2022 13:24:40 +0100 Subject: [PATCH 3/9] Another fix based now on a life Pi system - fixed storage of femrenter_data.json -> Fermenters are in 'data' - fixed hysteresis --- cbpi/__init__.py | 2 +- cbpi/controller/fermentation_controller.py | 3 ++- cbpi/extension/FermenterHysteresis/__init__.py | 10 +++------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index b8b91bd..2926142 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.1.a2" +__version__ = "4.0.1.a3" diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index 4edcd5e..06abd36 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -253,8 +253,9 @@ class FermentationController: #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) def save(self): + data = dict(data=list(map(lambda item: item.to_dict(), self.data))) with open(self.path, "w") as file: - json.dump(list(map(lambda item: item.to_dict(), self.data)), file, indent=4, sort_keys=True) + json.dump(data, file, indent=4, sort_keys=True) async def create_step(self, id, step: Step): try: diff --git a/cbpi/extension/FermenterHysteresis/__init__.py b/cbpi/extension/FermenterHysteresis/__init__.py index 3f3200d..7fd0f6e 100644 --- a/cbpi/extension/FermenterHysteresis/__init__.py +++ b/cbpi/extension/FermenterHysteresis/__init__.py @@ -60,16 +60,12 @@ class FermenterHysteresis(CBPiFermenterLogic): self.fermenter = self.get_fermenter(self.id) self.heater = self.fermenter.heater self.cooler = self.fermenter.cooler - - target_temp = self.get_fermenter_target_temp(self.id) - if target_temp == 0: - await self.set_fermenter_target_temp(self.id,int(self.props.get("TargetTemp", 0))) - + while self.running == True: - sensor_value = self.get_sensor_value(self.fermenter.sensor).get("value") - target_temp = self.get_fermenter_target_temp(self.id) + sensor_value = float(self.get_sensor_value(self.fermenter.sensor).get("value")) + target_temp = float(self.get_fermenter_target_temp(self.id)) if sensor_value + self.heater_offset_min <= target_temp: if self.heater: From e43aaf4fcd7200ac5e0fdb571bdddbf649bd7b5e Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Sun, 2 Jan 2022 17:08:23 +0100 Subject: [PATCH 4/9] Fix for Property Type Fermenter -> allows usage of property.ferenter in sensors, steps,.... --- cbpi/__init__.py | 2 +- cbpi/api/dataclasses.py | 4 +++- cbpi/api/property.py | 3 ++- cbpi/controller/basic_controller2.py | 2 +- cbpi/controller/plugin_controller.py | 7 +++++++ cbpi/http_endpoints/http_system.py | 6 +++--- 6 files changed, 17 insertions(+), 7 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 2926142..cae6342 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.1.a3" +__version__ = "4.0.1.a4" diff --git a/cbpi/api/dataclasses.py b/cbpi/api/dataclasses.py index 1524aa5..0448cd9 100644 --- a/cbpi/api/dataclasses.py +++ b/cbpi/api/dataclasses.py @@ -137,7 +137,9 @@ class Fermenter: def __str__(self): - return "id={} name={} sensor={} heater={} cooler={} brewname={} props={} temp={} type={} steps={}".format(self.id, self.name, self.sensor, self.heater, self.cooler, self.brewname, self.props, self.target_temp, self.type, self.steps) + return "name={} props={} temp={}".format(self.name, self.props, self.target_temp) + +# return "id={} name={} sensor={} heater={} cooler={} brewname={} props={} temp={} type={} steps={}".format(self.id, self.name, self.sensor, self.heater, self.cooler, self.brewname, self.props, self.target_temp, self.type, self.steps) def to_dict(self): if self.instance is not None: diff --git a/cbpi/api/property.py b/cbpi/api/property.py index 354ca47..cb14304 100644 --- a/cbpi/api/property.py +++ b/cbpi/api/property.py @@ -125,4 +125,5 @@ class Property(object): PropertyType.__init__(self) self.label = label self.configurable = True - self.description = description \ No newline at end of file + self.description = description + diff --git a/cbpi/controller/basic_controller2.py b/cbpi/controller/basic_controller2.py index c89b149..022b329 100644 --- a/cbpi/controller/basic_controller2.py +++ b/cbpi/controller/basic_controller2.py @@ -2,7 +2,7 @@ import logging import os.path import json -from cbpi.api.dataclasses import Actor, Props +from cbpi.api.dataclasses import Fermenter, Actor, Props import sys, os import shortuuid import asyncio diff --git a/cbpi/controller/plugin_controller.py b/cbpi/controller/plugin_controller.py index 193b905..96fc637 100644 --- a/cbpi/controller/plugin_controller.py +++ b/cbpi/controller/plugin_controller.py @@ -102,6 +102,8 @@ class PluginController(): return {"label": p.label, "type": "sensor", "configurable": p.configurable, "description": p.description} elif isinstance(p, Property.Kettle): return {"label": p.label, "type": "kettle", "configurable": p.configurable, "description": p.description} + elif isinstance(p, Property.Fermenter): + return {"label": p.label, "type": "fermenter", "configurable": p.configurable, "description": p.description} def _parse_step_props(self, cls, name): @@ -162,6 +164,11 @@ class PluginController(): result["properties"].append( {"name": m, "label": t.label, "type": "kettle", "configurable": t.configurable, "description": t.description}) + elif isinstance(tmpObj.__getattribute__(m), Property.Fermenter): + t = tmpObj.__getattribute__(m) + result["properties"].append( + {"name": m, "label": t.label, "type": "fermenter", "configurable": t.configurable, + "description": t.description}) for method_name, method in cls.__dict__.items(): if hasattr(method, "action"): diff --git a/cbpi/http_endpoints/http_system.py b/cbpi/http_endpoints/http_system.py index dddfcdd..5f15f88 100644 --- a/cbpi/http_endpoints/http_system.py +++ b/cbpi/http_endpoints/http_system.py @@ -28,10 +28,10 @@ class SystemHttpEndpoints: description: successful operation """ try: - fermenter=self.cbpi.fermenter.get_state() - logging.info(fermenter) + sensor=self.cbpi.sensor.get_state() + logging.info(sensor) except: - logging.info("!!!!!!!!!!!!!!!!!!!!!!!!!Error get fermenter state!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") + logging.info("!!!!!!!!!!!!!!!!!!!!!!!!!Error get sensor state!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") return web.json_response(data=dict( actor=self.cbpi.actor.get_state(), From 51136aef887180d472f11b9be0227fb7257de9c3 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Mon, 3 Jan 2022 12:47:20 +0100 Subject: [PATCH 5/9] Unification of mqtt data -> sensor values are updated under sensordata/{sensorid} One topic for each sensor to keep data small (esp compatibility) actor, kettle, sensor, fermenter, steps are updated also for each id individually to keep packets small e.g. actorupdate/{actorid} This was proposed by Innuendo to ensure compatibility with the ESP based MQTTDevice --- cbpi/__init__.py | 2 +- cbpi/api/sensor.py | 3 ++- cbpi/controller/actor_controller.py | 8 +++---- cbpi/controller/basic_controller2.py | 3 ++- cbpi/controller/fermentation_controller.py | 3 ++- cbpi/controller/step_controller.py | 4 ++-- .../extension/FermenterHysteresis/__init__.py | 24 ++++++++++++++----- 7 files changed, 31 insertions(+), 16 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index cae6342..632deb3 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.1.a4" +__version__ = "4.0.1.a5" diff --git a/cbpi/api/sensor.py b/cbpi/api/sensor.py index a71b75a..76182d5 100644 --- a/cbpi/api/sensor.py +++ b/cbpi/api/sensor.py @@ -35,7 +35,8 @@ class CBPiSensor(CBPiBase, metaclass=ABCMeta): def push_update(self, value): try: self.cbpi.ws.send(dict(topic="sensorstate", id=self.id, value=value)) - self.cbpi.push_update("cbpi/sensor/{}/udpate".format(self.id), dict(id=self.id, value=value), retain=True) + self.cbpi.push_update("cbpi/sensordata/{}".format(self.id), dict(id=self.id, value=value), retain=True) +# self.cbpi.push_update("cbpi/sensor/{}/udpate".format(self.id), dict(id=self.id, value=value), retain=True) except: logging.error("Faild to push sensor update") diff --git a/cbpi/controller/actor_controller.py b/cbpi/controller/actor_controller.py index 75bd623..edb559d 100644 --- a/cbpi/controller/actor_controller.py +++ b/cbpi/controller/actor_controller.py @@ -21,7 +21,7 @@ class ActorController(BasicController): if item.instance.state is False: await item.instance.on(power) await self.push_udpate() - self.cbpi.push_update("cbpi/actor/"+id, item.to_dict(), True) + self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict(), True) else: await self.set_power(id, power) @@ -34,7 +34,7 @@ class ActorController(BasicController): if item.instance.state is True: await item.instance.off() await self.push_udpate() - self.cbpi.push_update("cbpi/actor/"+id, item.to_dict()) + self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict()) except Exception as e: logging.error("Failed to switch on Actor {} {}".format(id, e), True) @@ -43,7 +43,7 @@ class ActorController(BasicController): item = self.find_by_id(id) instance = item.get("instance") await instance.toggle() - self.cbpi.push_update("cbpi/actor/update", item.to_dict()) + self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict()) except Exception as e: logging.error("Failed to toggle Actor {} {}".format(id, e)) @@ -59,6 +59,6 @@ class ActorController(BasicController): item = self.find_by_id(id) item.power = round(power) await self.push_udpate() - self.cbpi.push_update("cbpi/actor/"+id, item.to_dict()) + self.cbpi.push_update("cbpi/actorupdate/{}".format(id), item.to_dict()) except Exception as e: logging.error("Failed to update Actor {} {}".format(id, e)) diff --git a/cbpi/controller/basic_controller2.py b/cbpi/controller/basic_controller2.py index 022b329..18c2914 100644 --- a/cbpi/controller/basic_controller2.py +++ b/cbpi/controller/basic_controller2.py @@ -55,7 +55,8 @@ class BasicController: async def push_udpate(self): self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) - self.cbpi.push_update("cbpi/{}/update".format(self.update_key), 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()) def find_by_id(self, id): return next((item for item in self.data if item.id == id), None) diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index 06abd36..d6ffd04 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -110,7 +110,8 @@ class FermentationController: def push_update(self): self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) - self.cbpi.push_update("cbpi/{}/update".format(self.update_key), 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()) pass async def shutdown(self, app=None): diff --git a/cbpi/controller/step_controller.py b/cbpi/controller/step_controller.py index c5caaf9..8f0537a 100644 --- a/cbpi/controller/step_controller.py +++ b/cbpi/controller/step_controller.py @@ -256,8 +256,8 @@ class StepController: self.cbpi.ws.send(dict(topic="mash_profile_update", data=self.get_state())) else: self.cbpi.ws.send(dict(topic="step_update", data=list(map(lambda item: item.to_dict(), self.profile)))) - - self.cbpi.push_update(topic="cbpi/stepupdate", data=list(map(lambda item: item.to_dict(), self.profile))) + for item in self.profile: + self.cbpi.push_update(topic="cbpi/stepupdate/{}".format(item.id), data=(item.to_dict())) async def start_step(self,step): try: diff --git a/cbpi/extension/FermenterHysteresis/__init__.py b/cbpi/extension/FermenterHysteresis/__init__.py index 7fd0f6e..4ed1561 100644 --- a/cbpi/extension/FermenterHysteresis/__init__.py +++ b/cbpi/extension/FermenterHysteresis/__init__.py @@ -46,7 +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 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.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)")]) class FermenterHysteresis(CBPiFermenterLogic): @@ -60,27 +61,38 @@ class FermenterHysteresis(CBPiFermenterLogic): self.fermenter = self.get_fermenter(self.id) self.heater = self.fermenter.heater self.cooler = self.fermenter.cooler - + + heater = self.cbpi.actor.find_by_id(self.heater) + cooler = self.cbpi.actor.find_by_id(self.cooler) while self.running == True: sensor_value = float(self.get_sensor_value(self.fermenter.sensor).get("value")) target_temp = float(self.get_fermenter_target_temp(self.id)) + try: + heater_state = heater.instance.state + except: + heater_state= False + try: + cooler_state = cooler.instance.state + except: + cooler_state= False + if sensor_value + self.heater_offset_min <= target_temp: - if self.heater: + if self.heater and (heater_state == False): await self.actor_on(self.heater) if sensor_value + self.heater_offset_max >= target_temp: - if self.heater: + if self.heater and (heater_state == True): await self.actor_off(self.heater) if sensor_value >= self.cooler_offset_min + target_temp: - if self.cooler: + if self.cooler and (cooler_state == False): await self.actor_on(self.cooler) if sensor_value <= self.cooler_offset_max + target_temp: - if self.cooler: + if self.cooler and (cooler_state == True): await self.actor_off(self.cooler) await asyncio.sleep(1) From 78a0832fd257c5295859852a750bc5f7e9b729f3 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Mon, 3 Jan 2022 17:21:32 +0100 Subject: [PATCH 6/9] Update __init__.py --- cbpi/extension/mashstep/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cbpi/extension/mashstep/__init__.py b/cbpi/extension/mashstep/__init__.py index ee7c924..c830de2 100644 --- a/cbpi/extension/mashstep/__init__.py +++ b/cbpi/extension/mashstep/__init__.py @@ -319,7 +319,7 @@ class ActorStep(CBPiStep): Property.Number(label="Temp", description="Boil temperature", configurable=True), Property.Sensor(label="Sensor"), Property.Kettle(label="Kettle"), - Property.Select(label="LidAlert",options=["Yes","No"], description="Trigger Alert to remove id if temp is close to boil"), + Property.Select(label="LidAlert",options=["Yes","No"], description="Trigger Alert to remove lid if temp is close to boil"), Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Kettlelogic automatically on and off -> Yes"), Property.Select("First_Wort", options=["Yes","No"], description="First Wort Hop alert if set to Yes"), Property.Number("Hop_1", configurable = True, description="First Hop alert (minutes before finish)"), From 49362aa09864d75ccc4b17b5359a639c3fb62fd7 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Mon, 3 Jan 2022 17:53:22 +0100 Subject: [PATCH 7/9] Fixed Recipe import in case of two kettles In case of different boil kettle, sensor from first kettle was used by external recipe import. Thi sis now fixed. In case of different boil kettle, Sensor specified for this kettle will be used during external recipe import --- cbpi/__init__.py | 2 +- cbpi/controller/upload_controller.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 632deb3..159cfe2 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.1.a5" +__version__ = "4.0.1.a6" diff --git a/cbpi/controller/upload_controller.py b/cbpi/controller/upload_controller.py index c0702ad..50bc45d 100644 --- a/cbpi/controller/upload_controller.py +++ b/cbpi/controller/upload_controller.py @@ -264,7 +264,7 @@ class UploadController: "props": { "AutoMode": self.AutoMode, "Kettle": self.boilid, - "Sensor": self.kettle.sensor, + "Sensor": self.boilkettle.sensor, "Temp": int(self.BoilTemp), "Timer": BoilTime, "First_Wort": FirstWort, @@ -393,7 +393,7 @@ class UploadController: step_type = self.boil if self.boil != "" else "BoilStep" step_time = str(int(boil_time)) step_temp = self.BoilTemp - sensor = self.kettle.sensor + sensor = self.boilkettle.sensor LidAlert = "Yes" step_string = { "name": "Boil Step", @@ -584,7 +584,7 @@ class UploadController: step_time = str(int(BoilTime)) step_type = self.boil if self.boil != "" else "BoilStep" step_temp = self.BoilTemp - sensor = self.kettle.sensor + sensor = self.boilkettle.sensor LidAlert = "Yes" step_string = { "name": "Boil Step", @@ -697,7 +697,7 @@ class UploadController: if step_type == "CooldownStep": cooldown_sensor = self.cbpi.config.get("steps_cooldown_sensor", None) if cooldown_sensor is None or cooldown_sensor == '': - cooldown_sensor = self.kettle.sensor # fall back to kettle sensor if no other sensor is specified + cooldown_sensor = self.boilkettle.sensor # fall back to boilkettle sensor if no other sensor is specified step_timer = "" step_temp = int(self.CoolDownTemp) step_string = { "name": "Cooldown", @@ -744,7 +744,7 @@ class UploadController: try: self.boilkettle = self.cbpi.kettle.find_by_id(self.boilid) except: - pass + self.boilkettle = self.kettle config_values = { "kettle": self.kettle, "kettle_id": str(self.id), From a8f7f2d92f6a578f8119626f9b466adad11ca0d5 Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Wed, 5 Jan 2022 06:46:35 +0100 Subject: [PATCH 8/9] Additional CBPIFermenterStep Class Class has been activated but is not yet used Fix in mqtt actor power action setting --- cbpi/__init__.py | 2 +- cbpi/api/__init__.py | 3 +- cbpi/api/exceptions.py | 5 +- cbpi/api/step.py | 2 +- cbpi/controller/fermentation_controller.py | 29 +-- cbpi/controller/plugin_controller.py | 5 +- cbpi/extension/FermentationStep/__init__.py | 231 ++++++++++++++++++++ cbpi/extension/FermentationStep/config.yaml | 3 + cbpi/extension/mqtt_actor/mqtt_actor.py | 2 +- cbpi/http_endpoints/http_system.py | 6 - 10 files changed, 257 insertions(+), 31 deletions(-) create mode 100644 cbpi/extension/FermentationStep/__init__.py create mode 100644 cbpi/extension/FermentationStep/config.yaml diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 159cfe2..e695e31 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.1.a6" +__version__ = "4.0.1.a7" diff --git a/cbpi/api/__init__.py b/cbpi/api/__init__.py index c62cd1a..d0da38b 100644 --- a/cbpi/api/__init__.py +++ b/cbpi/api/__init__.py @@ -15,7 +15,8 @@ __all__ = ["CBPiActor", "SensorException", "ActorException", "CBPiSensor", - "CBPiStep"] + "CBPiStep", + "CBPiFermentationStep"] from cbpi.api.actor import * from cbpi.api.sensor import * diff --git a/cbpi/api/exceptions.py b/cbpi/api/exceptions.py index acec4ee..a17a2a6 100644 --- a/cbpi/api/exceptions.py +++ b/cbpi/api/exceptions.py @@ -1,4 +1,4 @@ -__all__ = ["CBPiException","KettleException","SensorException","ActorException"] +__all__ = ["CBPiException","KettleException","FermenterException","SensorException","ActorException"] class CBPiException(Exception): @@ -7,6 +7,9 @@ class CBPiException(Exception): class KettleException(CBPiException): pass +class FermenterException(CBPiException): + pass + class SensorException(CBPiException): pass diff --git a/cbpi/api/step.py b/cbpi/api/step.py index b037b88..03f41f6 100644 --- a/cbpi/api/step.py +++ b/cbpi/api/step.py @@ -4,7 +4,7 @@ from abc import abstractmethod from cbpi.api.base import CBPiBase -__all__ = ["StepResult", "StepState", "StepMove", "CBPiStep"] +__all__ = ["StepResult", "StepState", "StepMove", "CBPiStep", "CBPiFermentationStep"] from enum import Enum diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index d6ffd04..ef455d2 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -11,7 +11,7 @@ from cbpi.api.dataclasses import Fermenter, FermenterStep, Props, Step from cbpi.controller.basic_controller2 import BasicController from tabulate import tabulate import sys, os -from ..api.step import CBPiStep, StepMove, StepResult, StepState +from ..api.step import CBPiStep, StepMove, StepResult, StepState, CBPiFermentationStep @@ -90,6 +90,7 @@ class FermentationController: self._loop = asyncio.get_event_loop() self.data = [] self.types = {} + self.steptypes = {} self.cbpi.app.on_cleanup.append(self.shutdown) async def init(self): @@ -171,8 +172,6 @@ class FermentationController: fermenter = Fermenter(id, name, sensor, heater, cooler, brewname, props, temp, logictype) fermenter.steps = list(map(lambda item: self._create_step(fermenter, item), data.get("steps", []))) self.push_update() - #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) - #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) return fermenter except: return @@ -185,18 +184,22 @@ class FermentationController: return list(map(lambda x: x.to_dict(), self.data)) def get_types(self): -# logging.info("{} Get Types".format(self.name)) result = {} for key, value in self.types.items(): result[key] = dict(name=value.get("name"), properties=value.get("properties"), actions=value.get("actions")) return result + def get_steptypes(self): + result = {} + for key, value in self.steptypes.items(): + result[key] = dict(name=value.get("name"), properties=value.get("properties"), actions=value.get("actions")) + return result + def get_state(self): -# logging.info("{} Get State".format(self.name)) if self.data == []: logging.info(self.data) - return {"data": list(map(lambda x: x.to_dict(), self.data)), "types":self.get_types()} + return {"data": list(map(lambda x: x.to_dict(), self.data)), "types":self.get_types(), "steptypes":self.get_steptypes()} async def get(self, id: str ): return self._find_by_id(id) @@ -206,8 +209,6 @@ class FermentationController: self.data.append(data) self.save() self.push_update() - #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) - #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) return data async def update(self, item: Fermenter ): @@ -228,8 +229,6 @@ class FermentationController: self.data = list(map(lambda old: _update(old, item) if old.id == item.id else old, self.data)) self.save() self.push_update() - #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) - #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) return item async def set_target_temp(self, id: str, target_temp): @@ -240,8 +239,6 @@ class FermentationController: item.target_temp = target_temp self.save() self.push_update() - #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) - #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) except Exception as e: logging.error("Failed to set Target Temp {} {}".format(id, e)) @@ -250,8 +247,6 @@ class FermentationController: self.data = list(filter(lambda item: item.id != id, self.data)) self.save() self.push_update() - #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) - #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) def save(self): data = dict(data=list(map(lambda item: item.to_dict(), self.data))) @@ -332,7 +327,6 @@ class FermentationController: logging.info("{} started {}".format(item.name, id)) -# await self.push_udpate() except Exception as e: logging.error("{} Cant start {} - {}".format(item.name, id, e)) @@ -340,16 +334,13 @@ class FermentationController: try: item = self._find_by_id(id) - #logging.info(item) if item.instance is None or item.instance.state == False: await self.start_logic(id) else: await item.instance.stop() self.push_update() - #self.cbpi.ws.send(dict(topic=self.update_key, data=list(map(lambda item: item.to_dict(), self.data)))) - #self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data))) - + except Exception as e: logging.error("Failed to switch on FermenterLogic {} {}".format(id, e)) diff --git a/cbpi/controller/plugin_controller.py b/cbpi/controller/plugin_controller.py index 96fc637..aafb65a 100644 --- a/cbpi/controller/plugin_controller.py +++ b/cbpi/controller/plugin_controller.py @@ -31,7 +31,7 @@ class PluginController(): try: logger.info("Trying to load plugin %s" % filename) data = load_config(os.path.join( - this_directory, "../extension/%s/config.yaml" % filename)) + this_directory, "../extension/%s/config.yaml" % filename)) if (data.get("active") is True and data.get("version") == 4): self.modules[filename] = import_module( "cbpi.extension.%s" % (filename)) @@ -83,6 +83,9 @@ class PluginController(): if issubclass(clazz, CBPiStep): self.cbpi.step.types[name] = self._parse_step_props(clazz, name) + if issubclass(clazz, CBPiFermentationStep): + self.cbpi.fermenter.steptypes[name] = self._parse_step_props(clazz, name) + if issubclass(clazz, CBPiExtension): self.c = clazz(self.cbpi) diff --git a/cbpi/extension/FermentationStep/__init__.py b/cbpi/extension/FermentationStep/__init__.py new file mode 100644 index 0000000..e4499cd --- /dev/null +++ b/cbpi/extension/FermentationStep/__init__.py @@ -0,0 +1,231 @@ +import asyncio + +from cbpi.api import parameters, Property, action +from cbpi.api.step import StepResult, CBPiFermentationStep +from cbpi.api.timer import Timer +from datetime import datetime +import time +from voluptuous.schema_builder import message +from cbpi.api.dataclasses import NotificationAction, NotificationType +from cbpi.api.dataclasses import Kettle, Props, Fermenter +from cbpi.api import * +import logging +from socket import timeout +from typing import KeysView +from cbpi.api.config import ConfigType +from cbpi.api.base import CBPiBase +import numpy as np +import warnings + + + +@parameters([Property.Text(label="Notification",configurable = True, description = "Text for notification"), + Property.Select(label="AutoNext",options=["Yes","No"], description="Automatically move to next step (Yes) or pause after Notification (No)")]) +class NotificationStep(CBPiFermentationStep): + + async def NextStep(self, **kwargs): + await self.next() + + async def on_timer_done(self,timer): + self.summary = self.props.get("Notification","") + + if self.AutoNext == True: + self.cbpi.notify(self.name, self.props.get("Notification",""), NotificationType.INFO) + await self.next() + else: + self.cbpi.notify(self.name, self.props.get("Notification",""), NotificationType.INFO, action=[NotificationAction("Next Step", self.NextStep)]) + await self.push_update() + + async def on_timer_update(self,timer, seconds): + await self.push_update() + + async def on_start(self): + self.summary="" + self.AutoNext = False if self.props.get("AutoNext", "No") == "No" else True + if self.timer is None: + self.timer = Timer(1 ,on_update=self.on_timer_update, on_done=self.on_timer_done) + await self.push_update() + + async def on_stop(self): + await self.timer.stop() + self.summary = "" + await self.push_update() + + async def run(self): + while self.running == True: + await asyncio.sleep(1) + if self.timer.is_running is not True: + self.timer.start() + self.timer.is_running = True + + return StepResult.DONE + +@parameters([Property.Number(label="Temp", configurable=True), + Property.Sensor(label="Sensor"), + Property.Kettle(label="Kettle"), + Property.Text(label="Notification",configurable = True, description = "Text for notification when Temp is reached"), + Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Kettlelogic automatically on and off -> Yes")]) +class TargetTempStep(CBPiFermentationStep): + + async def NextStep(self, **kwargs): + await self.next() + + async def on_timer_done(self,timer): + self.summary = "" + self.kettle.target_temp = 0 + await self.push_update() + if self.AutoMode == True: + await self.setAutoMode(False) + self.cbpi.notify(self.name, self.props.get("Notification","Target Temp reached. Please add malt and klick next to move on."), action=[NotificationAction("Next Step", self.NextStep)]) + + async def on_timer_update(self,timer, seconds): + await self.push_update() + + async def on_start(self): + self.AutoMode = True if self.props.get("AutoMode","No") == "Yes" else False + self.kettle=self.get_kettle(self.props.get("Kettle", None)) + if self.kettle is not None: + self.kettle.target_temp = int(self.props.get("Temp", 0)) + if self.AutoMode == True: + await self.setAutoMode(True) + self.summary = "Waiting for Target Temp" + if self.cbpi.kettle is not None and self.timer is None: + self.timer = Timer(1 ,on_update=self.on_timer_update, on_done=self.on_timer_done) + await self.push_update() + + async def on_stop(self): + await self.timer.stop() + self.summary = "" + if self.AutoMode == True: + await self.setAutoMode(False) + await self.push_update() + + async def run(self): + while self.running == True: + await asyncio.sleep(1) + sensor_value = self.get_sensor_value(self.props.get("Sensor", None)).get("value") + if sensor_value >= int(self.props.get("Temp",0)) and self.timer.is_running is not True: + self.timer.start() + self.timer.is_running = True + await self.push_update() + return StepResult.DONE + + async def reset(self): + self.timer = Timer(1 ,on_update=self.on_timer_update, on_done=self.on_timer_done) + + async def setAutoMode(self, auto_state): + try: + if (self.kettle.instance is None or self.kettle.instance.state == False) and (auto_state is True): + await self.cbpi.kettle.toggle(self.kettle.id) + elif (self.kettle.instance.state == True) and (auto_state is False): + await self.cbpi.kettle.stop(self.kettle.id) + await self.push_update() + + except Exception as e: + logging.error("Failed to switch on KettleLogic {} {}".format(self.kettle.id, e)) + + +@parameters([Property.Number(label="Timer", description="Time in Minutes", configurable=True), + Property.Number(label="Temp", configurable=True), + Property.Sensor(label="Sensor"), + Property.Kettle(label="Kettle"), + Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Kettlelogic automatically on and off -> Yes")]) +class FermentationStep(CBPiFermentationStep): + + @action("Start Timer", []) + async def start_timer(self): + if self.timer.is_running is not True: + self.cbpi.notify(self.name, 'Timer started', NotificationType.INFO) + self.timer.start() + self.timer.is_running = True + else: + self.cbpi.notify(self.name, 'Timer is already running', NotificationType.WARNING) + + @action("Add 5 Minutes to Timer", []) + async def add_timer(self): + if self.timer.is_running == True: + self.cbpi.notify(self.name, '5 Minutes added', NotificationType.INFO) + await self.timer.add(300) + else: + self.cbpi.notify(self.name, 'Timer must be running to add time', NotificationType.WARNING) + + + async def on_timer_done(self,timer): + self.summary = "" + self.kettle.target_temp = 0 + if self.AutoMode == True: + await self.setAutoMode(False) + self.cbpi.notify(self.name, 'Step finished', NotificationType.SUCCESS) + + await self.next() + + async def on_timer_update(self,timer, seconds): + self.summary = Timer.format_time(seconds) + await self.push_update() + + async def on_start(self): + self.AutoMode = True if self.props.get("AutoMode", "No") == "Yes" else False + self.kettle=self.get_kettle(self.props.Kettle) + if self.kettle is not None: + self.kettle.target_temp = int(self.props.get("Temp", 0)) + if self.AutoMode == True: + await self.setAutoMode(True) + await self.push_update() + + if self.cbpi.kettle is not None and self.timer is None: + self.timer = Timer(int(self.props.get("Timer",0)) *60 ,on_update=self.on_timer_update, on_done=self.on_timer_done) + elif self.cbpi.kettle is not None: + try: + if self.timer.is_running == True: + self.timer.start() + except: + pass + + self.summary = "Waiting for Target Temp" + await self.push_update() + + async def on_stop(self): + await self.timer.stop() + self.summary = "" + if self.AutoMode == True: + await self.setAutoMode(False) + await self.push_update() + + async def reset(self): + self.timer = Timer(int(self.props.get("Timer",0)) *60 ,on_update=self.on_timer_update, on_done=self.on_timer_done) + + async def run(self): + while self.running == True: + await asyncio.sleep(1) + sensor_value = self.get_sensor_value(self.props.get("Sensor", None)).get("value") + if sensor_value >= int(self.props.get("Temp",0)) and self.timer.is_running is not True: + self.timer.start() + self.timer.is_running = True + estimated_completion_time = datetime.fromtimestamp(time.time()+ (int(self.props.get("Timer",0)))*60) + self.cbpi.notify(self.name, 'Timer started. Estimated completion: {}'.format(estimated_completion_time.strftime("%H:%M")), NotificationType.INFO) + return StepResult.DONE + + async def setAutoMode(self, auto_state): + try: + if (self.kettle.instance is None or self.kettle.instance.state == False) and (auto_state is True): + await self.cbpi.kettle.toggle(self.kettle.id) + elif (self.kettle.instance.state == True) and (auto_state is False): + await self.cbpi.kettle.stop(self.kettle.id) + await self.push_update() + + except Exception as e: + logging.error("Failed to switch on KettleLogic {} {}".format(self.kettle.id, e)) + + +def setup(cbpi): + ''' + This method is called by the server during startup + Here you need to register your plugins at the server + + :param cbpi: the cbpi core + :return: + ''' + + cbpi.plugin.register("NotificationStep", NotificationStep) + cbpi.plugin.register("TargetTempStep", TargetTempStep) + cbpi.plugin.register("FermentationStep", FermentationStep) \ No newline at end of file diff --git a/cbpi/extension/FermentationStep/config.yaml b/cbpi/extension/FermentationStep/config.yaml new file mode 100644 index 0000000..877138a --- /dev/null +++ b/cbpi/extension/FermentationStep/config.yaml @@ -0,0 +1,3 @@ +name: FermentationStep +version: 4 +active: true \ No newline at end of file diff --git a/cbpi/extension/mqtt_actor/mqtt_actor.py b/cbpi/extension/mqtt_actor/mqtt_actor.py index e9a6f4a..5788c12 100644 --- a/cbpi/extension/mqtt_actor/mqtt_actor.py +++ b/cbpi/extension/mqtt_actor/mqtt_actor.py @@ -9,7 +9,7 @@ class MQTTActor(CBPiActor): # Custom property which can be configured by the user @action("Set Power", parameters=[Property.Number(label="Power", configurable=True, description="Power Setting [0-100]")]) async def setpower(self,Power = 100 ,**kwargs): - self.power=round(Power) + self.power=int(Power) if self.power < 0: self.power = 0 if self.power > 100: diff --git a/cbpi/http_endpoints/http_system.py b/cbpi/http_endpoints/http_system.py index 5f15f88..629077d 100644 --- a/cbpi/http_endpoints/http_system.py +++ b/cbpi/http_endpoints/http_system.py @@ -27,12 +27,6 @@ class SystemHttpEndpoints: "200": description: successful operation """ - try: - sensor=self.cbpi.sensor.get_state() - logging.info(sensor) - except: - logging.info("!!!!!!!!!!!!!!!!!!!!!!!!!Error get sensor state!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") - return web.json_response(data=dict( actor=self.cbpi.actor.get_state(), fermenter=self.cbpi.fermenter.get_state(), From dfa5df2ef14873bb098f5629acdbe8c33f20048f Mon Sep 17 00:00:00 2001 From: avollkopf <43980694+avollkopf@users.noreply.github.com> Date: Tue, 11 Jan 2022 06:59:16 +0100 Subject: [PATCH 9/9] Some fixes - Different names for fermentersteps as Notificationstep caused an issue (steps not yot implemented for fermentation) - Reverted back item based mqtt as this may require further testing -> will be activated in dev branch later --- cbpi/__init__.py | 2 +- cbpi/controller/basic_controller2.py | 5 +++-- cbpi/controller/fermentation_controller.py | 6 ++++-- cbpi/controller/step_controller.py | 8 +++++--- cbpi/extension/FermentationStep/__init__.py | 6 +++--- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/cbpi/__init__.py b/cbpi/__init__.py index e695e31..ef9e0d7 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.1.a7" +__version__ = "4.0.1.0" diff --git a/cbpi/controller/basic_controller2.py b/cbpi/controller/basic_controller2.py index 18c2914..0444da9 100644 --- a/cbpi/controller/basic_controller2.py +++ b/cbpi/controller/basic_controller2.py @@ -55,8 +55,9 @@ class BasicController: async def push_udpate(self): self.cbpi.ws.send(dict(topic=self.update_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()) + self.cbpi.push_update("cbpi/{}".format(self.update_key), 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()) def find_by_id(self, id): return next((item for item in self.data if item.id == id), None) diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index ef455d2..16c152e 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -111,8 +111,10 @@ class FermentationController: def push_update(self): self.cbpi.ws.send(dict(topic=self.update_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()) + self.cbpi.push_update("cbpi/{}".format(self.update_key), 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()) pass async def shutdown(self, app=None): diff --git a/cbpi/controller/step_controller.py b/cbpi/controller/step_controller.py index 8f0537a..1f35a35 100644 --- a/cbpi/controller/step_controller.py +++ b/cbpi/controller/step_controller.py @@ -72,7 +72,7 @@ class StepController: self._loop.create_task(self.start_step(active_step)) async def add(self, item: Step): - logging.debug("Add step") + logging.info("Add step") item.id = shortuuid.uuid() item.status = StepState.INITIAL try: @@ -256,8 +256,10 @@ class StepController: self.cbpi.ws.send(dict(topic="mash_profile_update", data=self.get_state())) else: self.cbpi.ws.send(dict(topic="step_update", data=list(map(lambda item: item.to_dict(), self.profile)))) - for item in self.profile: - self.cbpi.push_update(topic="cbpi/stepupdate/{}".format(item.id), data=(item.to_dict())) + + self.cbpi.push_update(topic="cbpi/stepupdate", data=list(map(lambda item: item.to_dict(), self.profile))) + #for item in self.profile: + # self.cbpi.push_update(topic="cbpi/stepupdate/{}".format(item.id), data=(item.to_dict())) async def start_step(self,step): try: diff --git a/cbpi/extension/FermentationStep/__init__.py b/cbpi/extension/FermentationStep/__init__.py index e4499cd..93b8430 100644 --- a/cbpi/extension/FermentationStep/__init__.py +++ b/cbpi/extension/FermentationStep/__init__.py @@ -21,7 +21,7 @@ import warnings @parameters([Property.Text(label="Notification",configurable = True, description = "Text for notification"), Property.Select(label="AutoNext",options=["Yes","No"], description="Automatically move to next step (Yes) or pause after Notification (No)")]) -class NotificationStep(CBPiFermentationStep): +class FermenterNotificationStep(CBPiFermentationStep): async def NextStep(self, **kwargs): await self.next() @@ -65,7 +65,7 @@ class NotificationStep(CBPiFermentationStep): Property.Kettle(label="Kettle"), Property.Text(label="Notification",configurable = True, description = "Text for notification when Temp is reached"), Property.Select(label="AutoMode",options=["Yes","No"], description="Switch Kettlelogic automatically on and off -> Yes")]) -class TargetTempStep(CBPiFermentationStep): +class FermenterTargetTempStep(CBPiFermentationStep): async def NextStep(self, **kwargs): await self.next() @@ -228,4 +228,4 @@ def setup(cbpi): cbpi.plugin.register("NotificationStep", NotificationStep) cbpi.plugin.register("TargetTempStep", TargetTempStep) - cbpi.plugin.register("FermentationStep", FermentationStep) \ No newline at end of file + cbpi.plugin.register("FermentationStep", FermentationStep)