From 70469adc493a011caaa9cb3440b102983fff2032 Mon Sep 17 00:00:00 2001 From: Manuel Fritsch Date: Sun, 4 Apr 2021 15:54:10 +0200 Subject: [PATCH] "fermentation controller pre version. Not ready to use" --- cbpi/__init__.py | 2 +- cbpi/api/dataclasses.py | 39 ++- cbpi/api/step.py | 35 ++- cbpi/controller/fermentation_controller.py | 287 ++++++++++++++++++ cbpi/controller/step_controller.py | 3 +- tests/test_actor.py | 1 + tests/test_fermenter.py | 116 +++++++ .../cbpi/controller/step_controller.py | 2 +- 8 files changed, 471 insertions(+), 14 deletions(-) create mode 100644 cbpi/controller/fermentation_controller.py create mode 100644 tests/test_fermenter.py diff --git a/cbpi/__init__.py b/cbpi/__init__.py index e16d4b8..f296219 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.0.33" \ No newline at end of file +__version__ = "4.0.0.34" \ No newline at end of file diff --git a/cbpi/api/dataclasses.py b/cbpi/api/dataclasses.py index 52445a0..c21e0ce 100644 --- a/cbpi/api/dataclasses.py +++ b/cbpi/api/dataclasses.py @@ -2,7 +2,8 @@ from cbpi.api.config import ConfigType from enum import Enum from typing import Any from cbpi.api.step import StepState -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import List class Props: @@ -96,11 +97,13 @@ class Kettle: if self.instance is not None: state = self.instance.state - print("READ STATE", state) + else: 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()) + + @dataclass class Step: id: str = None @@ -115,7 +118,37 @@ class Step: 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()) + +@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()) diff --git a/cbpi/api/step.py b/cbpi/api/step.py index 8e16b07..b037b88 100644 --- a/cbpi/api/step.py +++ b/cbpi/api/step.py @@ -1,4 +1,5 @@ import asyncio +import logging from abc import abstractmethod from cbpi.api.base import CBPiBase @@ -7,6 +8,11 @@ __all__ = ["StepResult", "StepState", "StepMove", "CBPiStep"] 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): STOP = 1 @@ -39,12 +45,20 @@ class CBPiStep(CBPiBase): self.props = props self.cancel_reason: StepResult = None self.summary = "" + self.task = None self.running: bool = False + self.logger = logging.getLogger(__name__) 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): + self.logger.info("Start {}".format(self.name)) self.running = True self.task = asyncio.create_task(self._run()) self.task.add_done_callback(self._done) @@ -58,12 +72,13 @@ class CBPiStep(CBPiBase): async def stop(self): try: self.running = False - self.cancel_reason = StepResult.STOP - self.task.cancel() - await self.task - except: - pass - + if self.task is not None and self.task.done() is False: + self.cancel_reason = StepResult.STOP + self.task.cancel() + await self.task + except Exception as e: + self.logger.error(e) + async def reset(self): pass @@ -100,3 +115,9 @@ class CBPiStep(CBPiBase): def __str__(self): 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) \ No newline at end of file diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py new file mode 100644 index 0000000..057cdd2 --- /dev/null +++ b/cbpi/controller/fermentation_controller.py @@ -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) + + diff --git a/cbpi/controller/step_controller.py b/cbpi/controller/step_controller.py index f7d9c57..aefbf3b 100644 --- a/cbpi/controller/step_controller.py +++ b/cbpi/controller/step_controller.py @@ -23,7 +23,6 @@ class StepController: self.basic_data = {} self.step = None self.types = {} - self.cbpi.app.on_cleanup.append(self.shutdown) async def init(self): @@ -225,7 +224,7 @@ class StepController: for p in self.profile: instance = p.instance # 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") await instance.stop() await instance.task diff --git a/tests/test_actor.py b/tests/test_actor.py index 14815e1..d808fe4 100644 --- a/tests/test_actor.py +++ b/tests/test_actor.py @@ -1,5 +1,6 @@ import logging from unittest import mock +from unittest.mock import MagicMock, Mock from aiohttp.test_utils import AioHTTPTestCase, unittest_run_loop from cbpi.craftbeerpi import CraftBeerPi diff --git a/tests/test_fermenter.py b/tests/test_fermenter.py new file mode 100644 index 0000000..96ccfc0 --- /dev/null +++ b/tests/test_fermenter.py @@ -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() diff --git a/venv3/lib/python3.7/site-packages/cbpi/controller/step_controller.py b/venv3/lib/python3.7/site-packages/cbpi/controller/step_controller.py index 7a0ebe7..d3269d0 100644 --- a/venv3/lib/python3.7/site-packages/cbpi/controller/step_controller.py +++ b/venv3/lib/python3.7/site-packages/cbpi/controller/step_controller.py @@ -225,7 +225,7 @@ class StepController: for p in self.profile: instance = p.instance # 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") await instance.stop() await instance.task