Merge pull request #27 from avollkopf/development

Merge from Development Branch -> Add Fermenter Class
This commit is contained in:
Alexander Vollkopf 2022-01-11 07:03:23 +01:00 committed by GitHub
commit 029a6c280e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 949 additions and 62 deletions

View file

@ -1 +1 @@
__version__ = "4.0.0.59"
__version__ = "4.0.1.0"

View file

@ -9,12 +9,14 @@ __all__ = ["CBPiActor",
"parameters",
"background_task",
"CBPiKettleLogic",
"CBPiFermenterLogic",
"CBPiException",
"KettleException",
"SensorException",
"ActorException",
"CBPiSensor",
"CBPiStep"]
"CBPiStep",
"CBPiFermentationStep"]
from cbpi.api.actor import *
from cbpi.api.sensor import *
@ -22,5 +24,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 *

View file

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

View file

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

View file

@ -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,32 @@ 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 "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:
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 +179,7 @@ class ConfigType(Enum):
NUMBER="number"
SELECT="select"
STEP="step"
FERMENTER="fermenter"
@dataclass
class Config:

View file

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

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

@ -108,4 +108,22 @@ class Property(object):
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description
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=""):
'''
:param label:
:param description:
'''
PropertyType.__init__(self)
self.label = label
self.configurable = True
self.description = description

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
@ -55,7 +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))))
self.cbpi.push_update("cbpi/{}/update".format(self.update_key), list(map(lambda item: item.to_dict(), self.data)))
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)

View file

@ -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
from ..api.step import CBPiStep, StepMove, StepResult, StepState, CBPiFermentationStep
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,17 +80,43 @@ 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")
self._loop = asyncio.get_event_loop()
self.data = {}
self.data = []
self.types = {}
self.steptypes = {}
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/{}".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):
self.save()
for fermenter in self.data:
@ -108,13 +129,15 @@ class FermenationController:
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 i in data["data"]:
self.data.append(self._create(i))
def _create_step(self, fermenter, item):
id = item.get("id")
name = item.get("name")
@ -138,22 +161,47 @@ class FermenationController:
asyncio.create_task(self.start(step_instance.step.fermenter.id))
def _create(self, data):
id = data.get("id")
name = data.get("name")
brewname = data.get("brewname")
props = Props(data.get("props", {}))
fermenter = Fermenter(id, name, brewname, props, 0)
fermenter.steps = list(map(lambda item: self._create_step(fermenter, item), data.get("steps", [])))
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()
return fermenter
except:
return
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):
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):
if self.data == []:
logging.info(self.data)
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)
@ -162,12 +210,19 @@ class FermenationController:
data.id = shortuuid.uuid()
self.data.append(data)
self.save()
self.push_update()
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,23 +230,37 @@ 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()
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()
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()
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:
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 +292,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 +310,42 @@ 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))
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)
if item.instance is None or item.instance.state == False:
await self.start_logic(id)
else:
await item.instance.stop()
self.push_update()
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))
@ -284,4 +390,3 @@ class FermenationController:
except Exception as e:
self.logger.error(e)

View file

@ -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,12 +74,18 @@ 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)
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)
@ -100,6 +105,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):
@ -160,6 +167,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"):

View file

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

View file

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

View file

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

View file

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

View file

@ -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 FermenterNotificationStep(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 FermenterTargetTempStep(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)

View file

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

View file

@ -0,0 +1,125 @@
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
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"),
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.Sensor(label="sensor2",description="Optional Sensor for LCDisplay(e.g. iSpindle)")])
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
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 and (heater_state == False):
await self.actor_on(self.heater)
if sensor_value + self.heater_offset_max >= target_temp:
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 and (cooler_state == False):
await self.actor_on(self.cooler)
if sensor_value <= self.cooler_offset_max + target_temp:
if self.cooler and (cooler_state == True):
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

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

View file

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

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