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
This commit is contained in:
avollkopf 2022-01-02 11:25:56 +01:00
parent bee645ff96
commit d7c1b64493
17 changed files with 651 additions and 29 deletions

View file

@ -1 +1 @@
__version__ = "4.0.0.59" __version__ = "4.0.1.a1"

View file

@ -9,6 +9,7 @@ __all__ = ["CBPiActor",
"parameters", "parameters",
"background_task", "background_task",
"CBPiKettleLogic", "CBPiKettleLogic",
"CBPiFermenterLogic",
"CBPiException", "CBPiException",
"KettleException", "KettleException",
"SensorException", "SensorException",
@ -22,5 +23,6 @@ from cbpi.api.extension import *
from cbpi.api.property import * from cbpi.api.property import *
from cbpi.api.decorator import * from cbpi.api.decorator import *
from cbpi.api.kettle_logic import * from cbpi.api.kettle_logic import *
from cbpi.api.fermenter_logic import *
from cbpi.api.step import * from cbpi.api.step import *
from cbpi.api.exceptions import * from cbpi.api.exceptions import *

View file

@ -29,6 +29,15 @@ class CBPiBase(metaclass=ABCMeta):
async def set_target_temp(self,id, temp): async def set_target_temp(self,id, temp):
await self.cbpi.kettle.set_target_temp(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): def get_sensor(self,id):
return self.cbpi.sensor.find_by_id(id) return self.cbpi.sensor.find_by_id(id)

View file

@ -8,5 +8,6 @@ class ConfigType(Enum):
ACTOR = "actor" ACTOR = "actor"
SENSOR = "sensor" SENSOR = "sensor"
STEP = "step" STEP = "step"
FERMENTER = "fermenter"

View file

@ -62,7 +62,7 @@ class Actor:
def __str__(self): def __str__(self):
return "name={} props={}, state={}, type={}, power={}".format(self.name, self.props, self.state, self.type, self.power) return "name={} props={}, state={}, type={}, power={}".format(self.name, self.props, self.state, self.type, self.power)
def to_dict(self): 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 @dataclass
@ -125,15 +125,30 @@ class Step:
class Fermenter: class Fermenter:
id: str = None id: str = None
name: str = None name: str = None
sensor: Sensor = None
heater: Actor = None
cooler: Actor = None
brewname: str = None brewname: str = None
props: Props = Props() props: Props = Props()
target_temp: int = 0 target_temp: int = 0
type: str = None
steps: List[Step]= field(default_factory=list) steps: List[Step]= field(default_factory=list)
instance: str = None
def __str__(self): 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): 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)) 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 @dataclass
@ -162,6 +177,7 @@ class ConfigType(Enum):
NUMBER="number" NUMBER="number"
SELECT="select" SELECT="select"
STEP="step" STEP="step"
FERMENTER="fermenter"
@dataclass @dataclass
class Config: class Config:

View file

@ -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

View file

@ -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 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=""): def __init__(self, label, description=""):
''' '''

View file

@ -42,6 +42,11 @@ def create_config_file():
destfile = os.path.join(".", 'config') destfile = os.path.join(".", 'config')
shutil.copy(srcfile, destfile) 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: 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") srcfile = os.path.join(os.path.dirname(__file__), "config", "step_data.json")
destfile = os.path.join(".", 'config') destfile = os.path.join(".", 'config')

View file

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

View file

@ -1,4 +1,3 @@
import asyncio import asyncio
import cbpi import cbpi
import copy import copy
@ -9,19 +8,15 @@ from os import listdir
from os.path import isfile, join from os.path import isfile, join
import shortuuid import shortuuid
from cbpi.api.dataclasses import Fermenter, FermenterStep, Props, Step from cbpi.api.dataclasses import Fermenter, FermenterStep, Props, Step
from cbpi.controller.basic_controller2 import BasicController
from tabulate import tabulate from tabulate import tabulate
import sys, os import sys, os
from ..api.step import CBPiStep, StepMove, StepResult, StepState 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: class FermentStep:
def __init__(self, cbpi, step, on_done) -> None: def __init__(self, cbpi, step, on_done) -> None:
self.cbpi = cbpi self.cbpi = cbpi
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
@ -85,9 +80,10 @@ class FermentStep:
async def on_stop(self): async def on_stop(self):
pass pass
class FermenationController: class FermentationController:
def __init__(self, cbpi): def __init__(self, cbpi):
self.update_key = "fermenterupdate"
self.cbpi = cbpi self.cbpi = cbpi
self.logger = logging.getLogger(__name__) self.logger = logging.getLogger(__name__)
self.path = os.path.join(".", 'config', "fermenter_data.json") self.path = os.path.join(".", 'config', "fermenter_data.json")
@ -96,6 +92,27 @@ class FermenationController:
self.types = {} self.types = {}
self.cbpi.app.on_cleanup.append(self.shutdown) 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): async def shutdown(self, app=None):
self.save() self.save()
for fermenter in self.data: for fermenter in self.data:
@ -114,7 +131,11 @@ class FermenationController:
with open(self.path) as json_file: with open(self.path) as json_file:
d = json.load(json_file) d = json.load(json_file)
self.data = list(map(lambda item: self._create(item), d)) 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): def _create_step(self, fermenter, item):
id = item.get("id") id = item.get("id")
name = item.get("name") name = item.get("name")
@ -140,20 +161,37 @@ class FermenationController:
def _create(self, data): def _create(self, data):
id = data.get("id") id = data.get("id")
name = data.get("name") 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") brewname = data.get("brewname")
props = Props(data.get("props", {})) 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", []))) 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 return fermenter
def _find_by_id(self, id): def _find_by_id(self, id):
return next((item for item in self.data if item.id == id), None) return next((item for item in self.data if item.id == id), None)
async def init(self):
pass
async def get_all(self): 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 ): async def get(self, id: str ):
return self._find_by_id(id) return self._find_by_id(id)
@ -162,12 +200,21 @@ class FermenationController:
data.id = shortuuid.uuid() data.id = shortuuid.uuid()
self.data.append(data) self.data.append(data)
self.save() 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 return data
async def update(self, item: Fermenter ): async def update(self, item: Fermenter ):
logging.info(item)
def _update(old_item: Fermenter, item: Fermenter): def _update(old_item: Fermenter, item: Fermenter):
old_item.name = item.name 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.brewname = item.brewname
old_item.props = item.props old_item.props = item.props
old_item.target_temp = item.target_temp 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.data = list(map(lambda old: _update(old, item) if old.id == item.id else old, self.data))
self.save() 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 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 ): async def delete(self, id: str ):
item = self._find_by_id(id) item = self._find_by_id(id)
self.data = list(filter(lambda item: item.id != id, self.data)) self.data = list(filter(lambda item: item.id != id, self.data))
self.save() 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): def save(self):
with open(self.path, "w") as file: with open(self.path, "w") as file:
@ -191,7 +257,7 @@ class FermenationController:
step.id = shortuuid.uuid() step.id = shortuuid.uuid()
item = self._find_by_id(id) 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) item.steps.append(step)
self.save() self.save()
@ -223,10 +289,11 @@ class FermenationController:
if step is None: if step is None:
self.logger.info("No futher step to start") 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: except Exception as e:
self.logger.error(e) self.logger.error(e)
@ -240,6 +307,46 @@ class FermenationController:
except Exception as e: except Exception as e:
self.logger.error(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): async def next(self, id):
self.logger.info("Next {} ".format(id)) self.logger.info("Next {} ".format(id))
@ -283,5 +390,4 @@ class FermenationController:
except Exception as e: except Exception as e:
self.logger.error(e) self.logger.error(e)

View file

@ -31,8 +31,7 @@ class PluginController():
try: try:
logger.info("Trying to load plugin %s" % filename) logger.info("Trying to load plugin %s" % filename)
data = load_config(os.path.join( 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): if (data.get("active") is True and data.get("version") == 4):
self.modules[filename] = import_module( self.modules[filename] = import_module(
"cbpi.extension.%s" % (filename)) "cbpi.extension.%s" % (filename))
@ -75,6 +74,9 @@ class PluginController():
if issubclass(clazz, CBPiKettleLogic): if issubclass(clazz, CBPiKettleLogic):
self.cbpi.kettle.types[name] = self._parse_step_props(clazz, name) 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): if issubclass(clazz, CBPiSensor):
self.cbpi.sensor.types[name] = self._parse_step_props(clazz, name) self.cbpi.sensor.types[name] = self._parse_step_props(clazz, name)

View file

@ -26,6 +26,7 @@ from cbpi.controller.sensor_controller import SensorController
from cbpi.controller.step_controller import StepController from cbpi.controller.step_controller import StepController
from cbpi.controller.recipe_controller import RecipeController from cbpi.controller.recipe_controller import RecipeController
from cbpi.controller.upload_controller import UploadController from cbpi.controller.upload_controller import UploadController
from cbpi.controller.fermentation_controller import FermentationController
from cbpi.controller.system_controller import SystemController from cbpi.controller.system_controller import SystemController
from cbpi.controller.satellite_controller import SatelliteController 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_log import LogHttpEndpoints
from cbpi.http_endpoints.http_notification import NotificationHttpEndpoints from cbpi.http_endpoints.http_notification import NotificationHttpEndpoints
from cbpi.http_endpoints.http_upload import UploadHttpEndpoints from cbpi.http_endpoints.http_upload import UploadHttpEndpoints
from cbpi.http_endpoints.http_fermentation import FermentationHttpEndpoints
import shortuuid import shortuuid
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -115,8 +117,9 @@ class CraftBeerPi:
self.satellite = None self.satellite = None
if str(self.static_config.get("mqtt", False)).lower() == "true": if str(self.static_config.get("mqtt", False)).lower() == "true":
self.satellite: SatelliteController = SatelliteController(self) self.satellite: SatelliteController = SatelliteController(self)
self.dashboard = DashboardController(self) self.dashboard = DashboardController(self)
self.fermenter : FermentationController = FermentationController(self)
self.http_step = StepHttpEndpoints(self) self.http_step = StepHttpEndpoints(self)
self.http_recipe = RecipeHttpEndpoints(self) self.http_recipe = RecipeHttpEndpoints(self)
self.http_sensor = SensorHttpEndpoints(self) self.http_sensor = SensorHttpEndpoints(self)
@ -129,7 +132,7 @@ class CraftBeerPi:
self.http_log = LogHttpEndpoints(self) self.http_log = LogHttpEndpoints(self)
self.http_notification = NotificationHttpEndpoints(self) self.http_notification = NotificationHttpEndpoints(self)
self.http_upload = UploadHttpEndpoints(self) self.http_upload = UploadHttpEndpoints(self)
self.http_fermenter = FermentationHttpEndpoints(self)
self.login = Login(self) self.login = Login(self)
@ -279,7 +282,7 @@ class CraftBeerPi:
await self.kettle.init() await self.kettle.init()
await self.call_initializer(self.app) await self.call_initializer(self.app)
await self.dashboard.init() await self.dashboard.init()
await self.fermenter.init()
self._swagger_setup() self._swagger_setup()

View file

@ -1,9 +1,10 @@
import os, threading, time import os, threading, time, shutil
from aiohttp import web from aiohttp import web
import logging import logging
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import asyncio import asyncio
import random import random
import json
from cbpi.api import * from cbpi.api import *
from cbpi.api.config import ConfigType from cbpi.api.config import ConfigType
from cbpi.api.base import CBPiBase from cbpi.api.base import CBPiBase

View file

@ -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)

View file

@ -0,0 +1,3 @@
name: FermenterHysteresis
version: 4
active: true

View file

@ -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)

View file

@ -29,6 +29,7 @@ class SystemHttpEndpoints:
""" """
return web.json_response(data=dict( return web.json_response(data=dict(
actor=self.cbpi.actor.get_state(), actor=self.cbpi.actor.get_state(),
fermenter=self.cbpi.fermenter.get_state(),
sensor=self.cbpi.sensor.get_state(), sensor=self.cbpi.sensor.get_state(),
kettle=self.cbpi.kettle.get_state(), kettle=self.cbpi.kettle.get_state(),
step=self.cbpi.step.get_state(), step=self.cbpi.step.get_state(),