"fermentation controller pre version. Not ready to use"

This commit is contained in:
Manuel Fritsch 2021-04-04 15:54:10 +02:00
parent fb2793eb85
commit 70469adc49
8 changed files with 471 additions and 14 deletions

View file

@ -1 +1 @@
__version__ = "4.0.0.33" __version__ = "4.0.0.34"

View file

@ -2,7 +2,8 @@ from cbpi.api.config import ConfigType
from enum import Enum from enum import Enum
from typing import Any from typing import Any
from cbpi.api.step import StepState from cbpi.api.step import StepState
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import List
class Props: class Props:
@ -96,11 +97,13 @@ class Kettle:
if self.instance is not None: if self.instance is not None:
state = self.instance.state state = self.instance.state
print("READ STATE", state)
else: else:
state = False state = False
return dict(id=self.id, name=self.name, state=state, target_temp=self.target_temp, heater=self.heater, agitator=self.agitator, sensor=self.sensor, type=self.type, props=self.props.to_dict()) return dict(id=self.id, name=self.name, state=state, target_temp=self.target_temp, heater=self.heater, agitator=self.agitator, sensor=self.sensor, type=self.type, props=self.props.to_dict())
@dataclass @dataclass
class Step: class Step:
id: str = None id: str = None
@ -115,7 +118,37 @@ class Step:
def to_dict(self): def to_dict(self):
msg = self.instance.summary if self.instance is not None else "" msg = self.instance.summary if self.instance is not None else ""
return dict(id=self.id, name=self.name, state_text=msg, type=self.type, status=self.status.value, props=self.props.to_dict())
@dataclass
class Fermenter:
id: str = None
name: str = None
brewname: str = None
props: Props = Props()
target_temp: int = 0
steps: List[Step]= field(default_factory=list)
def __str__(self):
return "id={} name={} brewname={} props={} temp={} steps={}".format(self.id, self.name, self.brewname, self.props, self.target_temp, self.steps)
def to_dict(self):
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)
@dataclass
class FermenterStep:
id: str = None
name: str = None
fermenter: Fermenter = None
props: Props = Props()
type: str = None
status: StepState = StepState.INITIAL
instance: str = None
def __str__(self):
return "name={} props={}, type={}, instance={}".format(self.name, self.props, self.type, self.instance)
def to_dict(self):
msg = self.instance.summary if self.instance is not None else ""
return dict(id=self.id, name=self.name, state_text=msg, type=self.type, status=self.status.value, props=self.props.to_dict()) return dict(id=self.id, name=self.name, state_text=msg, type=self.type, status=self.status.value, props=self.props.to_dict())

View file

@ -1,4 +1,5 @@
import asyncio import asyncio
import logging
from abc import abstractmethod from abc import abstractmethod
from cbpi.api.base import CBPiBase from cbpi.api.base import CBPiBase
@ -7,6 +8,11 @@ __all__ = ["StepResult", "StepState", "StepMove", "CBPiStep"]
from enum import Enum from enum import Enum
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 StepResult(Enum): class StepResult(Enum):
STOP = 1 STOP = 1
@ -39,12 +45,20 @@ class CBPiStep(CBPiBase):
self.props = props self.props = props
self.cancel_reason: StepResult = None self.cancel_reason: StepResult = None
self.summary = "" self.summary = ""
self.task = None
self.running: bool = False self.running: bool = False
self.logger = logging.getLogger(__name__)
def _done(self, task): def _done(self, task):
self._done_callback(self, task.result()) if self._done_callback is not None:
try:
result = task.result()
self._done_callback(self, result)
except Exception as e:
self.logger.error(e)
async def start(self): async def start(self):
self.logger.info("Start {}".format(self.name))
self.running = True self.running = True
self.task = asyncio.create_task(self._run()) self.task = asyncio.create_task(self._run())
self.task.add_done_callback(self._done) self.task.add_done_callback(self._done)
@ -58,12 +72,13 @@ class CBPiStep(CBPiBase):
async def stop(self): async def stop(self):
try: try:
self.running = False self.running = False
self.cancel_reason = StepResult.STOP if self.task is not None and self.task.done() is False:
self.task.cancel() self.cancel_reason = StepResult.STOP
await self.task self.task.cancel()
except: await self.task
pass except Exception as e:
self.logger.error(e)
async def reset(self): async def reset(self):
pass pass
@ -100,3 +115,9 @@ class CBPiStep(CBPiBase):
def __str__(self): def __str__(self):
return "name={} props={}, type={}".format(self.name, self.props, self.__class__.__name__) return "name={} props={}, type={}".format(self.name, self.props, self.__class__.__name__)
class CBPiFermentationStep(CBPiStep):
def __init__(self, cbpi, fermenter, step, props, on_done) -> None:
self.fermenter = fermenter
super().__init__(cbpi, step.id, step.name, props, on_done)

View file

@ -0,0 +1,287 @@
import asyncio
import cbpi
import copy
import json
import logging
import os.path
from os import listdir
from os.path import isfile, join
import shortuuid
from cbpi.api.dataclasses import Fermenter, FermenterStep, Props, Step
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__)
self.step = step
self.props = step.props
self._done_callback = on_done
self.task = None
self.summary = ""
def _done(self, task):
if self._done_callback is not None:
try:
result = task.result()
self._done_callback(self, result)
except Exception as e:
self.logger.error(e)
async def run(self):
while True:
await asyncio.sleep(1)
async def _run(self):
try:
await self.on_start()
await self.run()
self.cancel_reason = StepResult.DONE
except asyncio.CancelledError as e:
pass
finally:
await self.on_stop()
return self.cancel_reason
async def start(self):
self.logger.info("Start {}".format(self.step.name))
self.running = True
self.task = asyncio.create_task(self._run())
self.task.add_done_callback(self._done)
async def next(self):
self.running = False
self.cancel_reason = StepResult.NEXT
self.task.cancel()
await self.task
async def stop(self):
try:
self.running = False
if self.task is not None and self.task.done() is False:
self.logger.info("Stopping Task")
self.cancel_reason = StepResult.STOP
self.task.cancel()
await self.task
except Exception as e:
self.logger.error(e)
async def on_start(self):
self.props.hello = "WOOHOo"
pass
async def on_stop(self):
pass
class FermenationController:
def __init__(self, cbpi):
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.types = {}
self.cbpi.app.on_cleanup.append(self.shutdown)
async def shutdown(self, app=None):
self.save()
for fermenter in self.data:
self.logger.info("Shutdown {}".format(fermenter.name))
for step in fermenter.steps:
try:
self.logger.info("Stop {}".format(step.name))
await step.instance.stop()
except Exception as e:
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)
with open(self.path) as json_file:
d = json.load(json_file)
self.data = list(map(lambda item: self._create(item), d))
def _create_step(self, fermenter, item):
id = item.get("id")
name = item.get("name")
status = StepState(item.get("status", "I"))
type = item.get("type")
type_cfg = self.types.get(type)
if type_cfg is not None:
inst = type_cfg.get("class")()
print(inst)
step = FermenterStep(id=id, name=name, type=type, status=status, instance=None, fermenter=fermenter)
step.instance = FermentStep( self.cbpi, step, self._done)
return step
def _done(self, step_instance, result):
step_instance.step.status = StepState.DONE
self.save()
if result == StepResult.NEXT:
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
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
async def get(self, id: str ):
return self._find_by_id(id)
async def create(self, data: Fermenter ):
data.id = shortuuid.uuid()
self.data.append(data)
self.save()
return data
async def update(self, item: Fermenter ):
def _update(old_item: Fermenter, item: Fermenter):
old_item.name = item.name
old_item.brewname = item.brewname
old_item.props = item.props
old_item.target_temp = item.target_temp
return old_item
self.data = list(map(lambda old: _update(old, item) if old.id == item.id else old, self.data))
self.save()
return item
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()
def save(self):
with open(self.path, "w") as file:
json.dump(list(map(lambda item: item.to_dict(), self.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)
item.steps.append(step)
self.save()
return step
except Exception as e:
self.logger.error(e)
async def update_step(self, id, step):
item = self._find_by_id(id)
item = list(map(lambda old: item if old.id == step.id else old, item.steps))
self.save()
async def delete_step(self, id, stepid):
item = self._find_by_id(id)
item.steps = list(filter(lambda item: item.id != stepid, item.steps))
self.save()
def _find_by_status(self, data, status):
return next((item for item in data if item.status == status), None)
def _find_step_by_id(self, data, id):
return next((item for item in data if item.id == id), None)
async def start(self, id):
self.logger.info("Start")
try:
item = self._find_by_id(id)
step = self._find_by_status(item.steps, StepState.INITIAL)
if step is None:
self.logger.info("No futher step to start")
await step.instance.start()
step.status = StepState.ACTIVE
self.save()
except Exception as e:
self.logger.error(e)
async def stop(self, id):
try:
item = self._find_by_id(id)
step = self._find_by_status(item.steps, StepState.ACTIVE)
await step.instance.stop()
step.status = StepState.STOP
self.save()
except Exception as e:
self.logger.error(e)
async def next(self, id):
self.logger.info("Next {} ".format(id))
try:
item = self._find_by_id(id)
step = self._find_by_status(item.steps, StepState.ACTIVE)
await step.instance.next()
except Exception as e:
self.logger.error(e)
async def reset(self, id):
self.logger.info("Reset")
try:
item = self._find_by_id(id)
for step in item.steps:
self.logger.info("Stopping Step {} {}".format(step.name, step.id))
try:
await step.instance.stop()
step.status = StepState.INITIAL
except Exception as e:
self.logger.error(e)
self.save()
except Exception as e:
self.logger.error(e)
async def move_step(self, fermenter_id, step_id, direction):
try:
fermenter = self._find_by_id(fermenter_id)
index = next((i for i, item in enumerate(fermenter.steps) if item.id == step_id), None)
if index == None:
return
if index == 0 and direction == -1:
return
if index == len(fermenter.steps)-1 and direction == 1:
return
fermenter.steps[index], fermenter.steps[index+direction] = fermenter.steps[index+direction], fermenter.steps[index]
self.save()
except Exception as e:
self.logger.error(e)

View file

@ -23,7 +23,6 @@ class StepController:
self.basic_data = {} self.basic_data = {}
self.step = None self.step = None
self.types = {} self.types = {}
self.cbpi.app.on_cleanup.append(self.shutdown) self.cbpi.app.on_cleanup.append(self.shutdown)
async def init(self): async def init(self):
@ -225,7 +224,7 @@ class StepController:
for p in self.profile: for p in self.profile:
instance = p.instance instance = p.instance
# Stopping all running task # Stopping all running task
if instance.task != None and instance.task.done() is False: if hasattr(instance, "task") and instance.task != None and instance.task.done() is False:
logging.info("Stop Step") logging.info("Stop Step")
await instance.stop() await instance.stop()
await instance.task await instance.task

View file

@ -1,5 +1,6 @@
import logging import logging
from unittest import mock from unittest import mock
from unittest.mock import MagicMock, Mock
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
from cbpi.craftbeerpi import CraftBeerPi from cbpi.craftbeerpi import CraftBeerPi

116
tests/test_fermenter.py Normal file
View file

@ -0,0 +1,116 @@
import asyncio
from cbpi.api.dataclasses import Fermenter, FermenterStep, Props, Step
import logging
from unittest import mock
from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop
from cbpi.craftbeerpi import CraftBeerPi
from cbpi.controller.fermentation_controller import FermenationController
import unittest
import json
from aiohttp import web
from unittest.mock import MagicMock, Mock
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s')
class FermenterTest(AioHTTPTestCase):
async def get_application(self):
app = web.Application()
return app
def create_file(self):
data = [
{
"id": "f1",
"name": "Fermenter1",
"props": {},
"steps": [
{
"id": "f1s1",
"name": "Step1",
"props": {},
"state_text": "",
"status": "I",
"type": "T2"
},
{
"id": "f1s2",
"name": "Step2",
"props": {},
"state_text": "",
"status": "I",
"type": "T1"
},
],
"target_temp": 0
},
{
"id": "f2",
"name": "Fermenter2",
"props": {},
"steps": [
{
"id": "f2s1",
"name": "Step1",
"props": {},
"state_text": "",
"status": "I",
"type": "T1"
},
{
"id": "f2s2",
"name": "Step2",
"props": {},
"state_text": "",
"status": "I",
"type": "T2"
},
],
"target_temp": 0
}
]
with open("./config/fermenter_data.json", "w") as file:
json.dump(data, file, indent=4, sort_keys=True)
@unittest_run_loop
async def test_actor_mock(self):
self.create_file()
mock = Mock()
f = FermenationController(mock)
f.types = {
"T1": {"name": "T2", "class": FermenterStep, "properties": [], "actions": []},
"T2": {"name": "T2", "class": FermenterStep, "properties": [], "actions": []}
}
await f.load()
#ferm = Fermenter(name="Maneul")
# item = await f.create(ferm)
# await f.create_step(item.id, Step(name="Manuel"))
# await f.delete(item.id)
item = await f.get("f1")
await f.start("f1")
await f.start("f2")
await asyncio.sleep(3)
# await f.create_step(item.id, Step(name="MANUEL", props=Props()))
#await f.start(item.id)
#await asyncio.sleep(1)
#await f.next(item.id)
#await asyncio.sleep(1)
#await f.next(item.id)
#await asyncio.sleep(1)
#await f.next(item.id)
#await asyncio.sleep(1)
#await f.move_step("f1", "f1s1", 1)
# await f.reset(item.id)
await f.shutdown()
if __name__ == '__main__':
unittest.main()

View file

@ -225,7 +225,7 @@ class StepController:
for p in self.profile: for p in self.profile:
instance = p.instance instance = p.instance
# Stopping all running task # Stopping all running task
if instance.task != None and instance.task.done() is False: if hasattr(instance, "task") and instance.task != None and instance.task.done() is False:
logging.info("Stop Step") logging.info("Stop Step")
await instance.stop() await instance.stop()
await instance.task await instance.task