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] 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(),