diff --git a/cbpi/__init__.py b/cbpi/__init__.py index 58ab0b3..a3b7053 100644 --- a/cbpi/__init__.py +++ b/cbpi/__init__.py @@ -1 +1 @@ -__version__ = "4.0.1.18.a9" +__version__ = "4.0.2.0.a1" diff --git a/cbpi/api/dataclasses.py b/cbpi/api/dataclasses.py index 421e23a..2cb1477 100644 --- a/cbpi/api/dataclasses.py +++ b/cbpi/api/dataclasses.py @@ -129,6 +129,7 @@ class Fermenter: heater: Actor = None cooler: Actor = None brewname: str = None + description : str = None props: Props = Props() target_temp: int = 0 type: str = None @@ -150,7 +151,7 @@ class Fermenter: state = False steps = list(map(lambda item: item.to_dict(), self.steps)) - 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) + return dict(id=self.id, name=self.name, state=state, sensor=self.sensor, heater=self.heater, cooler=self.cooler, brewname=self.brewname, description=self.description, props=self.props.to_dict() if self.props is not None else None, target_temp=self.target_temp, type=self.type, steps=steps) @dataclass diff --git a/cbpi/controller/fermentation_controller.py b/cbpi/controller/fermentation_controller.py index 67d91dd..d1ec5a2 100644 --- a/cbpi/controller/fermentation_controller.py +++ b/cbpi/controller/fermentation_controller.py @@ -3,8 +3,10 @@ import asyncio import cbpi import copy import json +import yaml import logging import os.path +import pathlib from os import listdir from os.path import isfile, join import shortuuid @@ -114,10 +116,22 @@ class FermentationController: } destfile = os.path.join(".", 'config', "fermenter_data.json") json.dump(data,open(destfile,'w'),indent=4, sort_keys=True) + + pathlib.Path(os.path.join(".", 'config/fermenterrecipes')).mkdir(parents=True, exist_ok=True) - async def shutdown(self, app=None): + async def shutdown(self, app=None, fermenterid=None): self.save() - for fermenter in self.data: + if (fermenterid == None): + 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) + else: + fermenter = self._find_by_id(fermenterid) self.logger.info("Shutdown {}".format(fermenter.name)) for step in fermenter.steps: try: @@ -126,6 +140,7 @@ class FermentationController: 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: @@ -170,8 +185,9 @@ class FermentationController: logictype = data.get("type") temp = data.get("target_temp") brewname = data.get("brewname") + description = data.get("description") props = Props(data.get("props", {})) - fermenter = Fermenter(id, name, sensor, heater, cooler, brewname, props, temp, logictype) + fermenter = Fermenter(id, name, sensor, heater, cooler, brewname, description, props, temp, logictype) fermenter.steps = list(map(lambda item: self._create_step(fermenter, item), data.get("steps", []))) self.push_update() return fermenter @@ -246,8 +262,6 @@ class FermentationController: 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 @@ -255,6 +269,7 @@ class FermentationController: old_item.cooler = item.cooler old_item.type = item.type old_item.brewname = item.brewname + old_item.description = item.description old_item.props = item.props old_item.target_temp = item.target_temp return old_item @@ -544,4 +559,44 @@ class FermentationController: logging.info(item) await item.instance.__getattribute__(action)(**parameter) except Exception as e: - logging.error("FermenterStep Controller - Failed to call action on {} {} {}".format(id, action, e)) \ No newline at end of file + logging.error("FermenterStep Controller - Failed to call action on {} {} {}".format(id, action, e)) + + # todo: Sensors may need to be removed when saving the recipe -> need to be replaced when assinging later to Fermenter with 'fermenter.sensor' + async def savetobook(self, fermenterid): + name = shortuuid.uuid() + path = os.path.join(".", 'config', "fermenterrecipes", "{}.yaml".format(name)) + fermenter=self._find_by_id(fermenterid) + try: + brewname = fermenter.brewname + description = fermenter.description + # todo add escription at later point of time, once description has been added to fermenter dataclass + except: + brewname = "" + description = "" + self.basic_data={"name": brewname, "description": description} + + try: + fermentersteps = fermenter.steps + except: + fermentersteps = [] + data = dict(basic=self.basic_data, steps=list(map(lambda item: item.to_dict(), fermentersteps))) + with open(path, "w") as file: + yaml.dump(data, file) + + async def load_recipe(self, data, fermenterid): + try: + await self.shutdown(None, fermenterid) + except: + pass + fermenter = self._find_by_id(fermenterid) + def add_runtime_data(item): + item["status"] = "I" + item["id"] = shortuuid.uuid() + item["props"]["Sensor"] = fermenter.sensor + list(map(lambda item: add_runtime_data(item), data.get("steps"))) + fermenter.description = data['basic']['desc'] + fermenter.brewname = data['basic']['name'] + fermenter.steps=[] + await self.update(fermenter) + for item in data.get("steps"): + await self.create_step(fermenterid, item) diff --git a/cbpi/controller/fermenter_recipe_controller.py b/cbpi/controller/fermenter_recipe_controller.py new file mode 100644 index 0000000..e0065c3 --- /dev/null +++ b/cbpi/controller/fermenter_recipe_controller.py @@ -0,0 +1,88 @@ + +import logging +import os.path +from os import listdir +from os.path import isfile, join +import json +import shortuuid +import yaml +from ..api.step import StepMove, StepResult, StepState + +import re + +class FermenterRecipeController: + + + def __init__(self, cbpi): + self.cbpi = cbpi + self.logger = logging.getLogger(__name__) + + def urlify(self, s): + + # Remove all non-word characters (everything except numbers and letters) + s = re.sub(r"[^\w\s]", '', s) + + # Replace all runs of whitespace with a single dash + s = re.sub(r"\s+", '-', s) + + return s + + async def create(self, name): + id = shortuuid.uuid() + path = os.path.join(".", 'config', "fermenterrecipes", "{}.yaml".format(id)) + data = dict(basic=dict(name=name), steps=[]) + with open(path, "w") as file: + yaml.dump(data, file) + return id + + async def save(self, name, data): + path = os.path.join(".", 'config', "fermenterrecipes", "{}.yaml".format(name)) + logging.info(data) + with open(path, "w") as file: + yaml.dump(data, file, indent=4, sort_keys=True) + + + async def get_recipes(self): + path = os.path.join(".", 'config', "fermenterrecipes") + onlyfiles = [os.path.splitext(f)[0] for f in listdir(path) if isfile(join(path, f)) and f.endswith(".yaml")] + + result = [] + for filename in onlyfiles: + recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % filename) + with open(recipe_path) as file: + data = yaml.load(file, Loader=yaml.FullLoader) + dataset = data["basic"] + dataset["file"] = filename + result.append(dataset) + logging.info(result) + return result + + async def get_by_name(self, name): + + recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % name) + with open(recipe_path) as file: + return yaml.load(file, Loader=yaml.FullLoader) + + + async def remove(self, name): + path = os.path.join(".", 'config', "fermenterrecipes", "{}.yaml".format(name)) + os.remove(path) + + + async def brew(self, name, fermenterid): + + recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % name) + logging.info(recipe_path) + with open(recipe_path) as file: + data = yaml.load(file, Loader=yaml.FullLoader) + await self.cbpi.fermenter.load_recipe(data, fermenterid) + + async def clone(self, id, new_name): + recipe_path = os.path.join(".", 'config', "fermenterrecipes", "%s.yaml" % id) + with open(recipe_path) as file: + data = yaml.load(file, Loader=yaml.FullLoader) + data["basic"]["name"] = new_name + new_id = shortuuid.uuid() + await self.save(new_id, data) + + return new_id diff --git a/cbpi/craftbeerpi.py b/cbpi/craftbeerpi.py index a390f00..fbbde3f 100644 --- a/cbpi/craftbeerpi.py +++ b/cbpi/craftbeerpi.py @@ -25,6 +25,7 @@ from cbpi.controller.plugin_controller import PluginController from cbpi.controller.sensor_controller import SensorController from cbpi.controller.step_controller import StepController from cbpi.controller.recipe_controller import RecipeController +from cbpi.controller.fermenter_recipe_controller import FermenterRecipeController from cbpi.controller.upload_controller import UploadController from cbpi.controller.fermentation_controller import FermentationController @@ -45,6 +46,7 @@ from cbpi.http_endpoints.http_kettle import KettleHttpEndpoints from cbpi.http_endpoints.http_sensor import SensorHttpEndpoints from cbpi.http_endpoints.http_step import StepHttpEndpoints from cbpi.http_endpoints.http_recipe import RecipeHttpEndpoints +from cbpi.http_endpoints.http_fermenterrecipe import FermenterRecipeHttpEndpoints from cbpi.http_endpoints.http_plugin import PluginHttpEndpoints from cbpi.http_endpoints.http_system import SystemHttpEndpoints from cbpi.http_endpoints.http_log import LogHttpEndpoints @@ -112,6 +114,7 @@ class CraftBeerPi: self.fermenter : FermentationController = FermentationController(self) self.step : StepController = StepController(self) self.recipe : RecipeController = RecipeController(self) + self.fermenterrecipe : FermenterRecipeController = FermenterRecipeController(self) self.upload : UploadController = UploadController(self) self.notification : NotificationController = NotificationController(self) self.satellite = None @@ -121,6 +124,7 @@ class CraftBeerPi: self.http_step = StepHttpEndpoints(self) self.http_recipe = RecipeHttpEndpoints(self) + self.http_fermenterrecipe = FermenterRecipeHttpEndpoints(self) self.http_sensor = SensorHttpEndpoints(self) self.http_config = ConfigHttpEndpoints(self) self.http_actor = ActorHttpEndpoints(self) diff --git a/cbpi/http_endpoints/http_fermentation.py b/cbpi/http_endpoints/http_fermentation.py index 8615737..1eec644 100644 --- a/cbpi/http_endpoints/http_fermentation.py +++ b/cbpi/http_endpoints/http_fermentation.py @@ -77,7 +77,7 @@ class FermentationHttpEndpoints(): 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")) + fermenter = Fermenter(id=id, name=data.get("name"), sensor=data.get("sensor"), heater=data.get("heater"), cooler=data.get("cooler"), brewname=data.get("brewname"), description=data.get("description"), 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()) @@ -115,7 +115,7 @@ class FermentationHttpEndpoints(): """ 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")) + fermenter = Fermenter(id=id, name=data.get("name"), sensor=data.get("sensor"), heater=data.get("heater"), cooler=data.get("cooler"), brewname=data.get("brewname"), description=data.get("description"), 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) @@ -513,30 +513,6 @@ class FermentationHttpEndpoints(): await self.controller.stop(fermenterid) return web.Response(status=200) - @request_mapping(path="/{id}/nextstep", method="POST", auth_required=False) - async def http_next_step(self, request): - - """ - --- - description: Stop steps for Fermenter with fermenterid - tags: - - Fermenter - parameters: - - name: "id" - in: "path" - description: "Fermenter ID" - required: true - type: "integer" - format: "int64" - responses: - "200": - description: successful operation - """ - - fermenterid= request.match_info['id'] - await self.controller.next(fermenterid) - return web.Response(status=200) - @request_mapping(path="/{id}/nextstep", method="POST", auth_required=False) async def http_next_step(self, request): @@ -620,4 +596,21 @@ class FermentationHttpEndpoints(): id = request.match_info['id'] await self.controller.call_action(id,data.get("action"), data.get("parameter",[])) + return web.Response(status=204) + + @request_mapping(path="/savetobook/{id}", method="POST", auth_required=False) + async def http_savetobook(self, request): + + """ + + --- + description: Save Active FermenterRecipe to Fermenter Recipe Book + tags: + - Fermenter + responses: + "204": + description: successful operation + """ + fermenterid = request.match_info['id'] + await self.controller.savetobook(fermenterid) return web.Response(status=204) \ No newline at end of file diff --git a/cbpi/http_endpoints/http_fermenterrecipe.py b/cbpi/http_endpoints/http_fermenterrecipe.py new file mode 100644 index 0000000..76dddd9 --- /dev/null +++ b/cbpi/http_endpoints/http_fermenterrecipe.py @@ -0,0 +1,172 @@ +from cbpi.controller.fermenter_recipe_controller import FermenterRecipeController +from cbpi.api.dataclasses import Props, Step +from aiohttp import web +from cbpi.api import * +import logging + +class FermenterRecipeHttpEndpoints(): + + def __init__(self, cbpi): + self.cbpi = cbpi + self.controller : FermenterRecipeController = cbpi.fermenterrecipe + self.cbpi.register(self, "/fermenterrecipe") + + @request_mapping(path="/", method="GET", auth_required=False) + async def http_get_all(self, request): + """ + --- + description: Get all recipes + tags: + - FermenterRecipe + responses: + "200": + description: successful operation + """ + return web.json_response(await self.controller.get_recipes()) + + @request_mapping(path="/{name}", method="GET", auth_required=False) + async def get_by_name(self, request): + """ + --- + description: Get all recipes + tags: + - FermenterRecipe + parameters: + - name: "name" + in: "path" + description: "Recipe Name" + required: true + type: "string" + responses: + "200": + description: successful operation + """ + name = request.match_info['name'] + return web.json_response(await self.controller.get_by_name(name)) + + @request_mapping(path="/create", method="POST", auth_required=False) + async def http_create(self, request): + + """ + --- + description: Add Recipe + tags: + - FermenterRecipe + + responses: + "200": + description: successful operation + """ + data = await request.json() + print(data) + return web.json_response(dict(id=await self.controller.create(data.get("name")))) + + + @request_mapping(path="/{name}", method="PUT", auth_required=False) + async def http_save(self, request): + + """ + --- + description: Save Recipe + tags: + - FermenterRecipe + parameters: + - name: "id" + in: "path" + description: "Recipe Id" + required: true + type: "string" + - in: body + name: body + description: Recipe Data + required: false + schema: + type: object + + responses: + "200": + description: successful operation + """ + data = await request.json() + name = request.match_info['name'] + await self.controller.save(name, data) + print(data) + return web.Response(status=204) + + @request_mapping(path="/{name}", method="DELETE", auth_required=False) + async def http_remove(self, request): + + """ + --- + description: Delete + tags: + - FermenterRecipe + parameters: + - name: "id" + in: "path" + description: "Recipe Id" + required: true + type: "string" + + + responses: + "200": + description: successful operation + """ + name = request.match_info['name'] + await self.controller.remove(name) + return web.Response(status=204) + + @request_mapping(path="/{name}/{fermenterid}/brew", method="POST", auth_required=False) + async def http_brew(self, request): + + """ + --- + description: Send Recipe to Fermenter + tags: + - FermenterRecipe + parameters: + - name: "name" + in: "path" + description: "Recipe Id" + required: true + type: "string" + + + responses: + "200": + description: successful operation + """ + name = request.match_info['name'] + fermenterid = request.match_info['fermenterid'] + await self.controller.brew(name,fermenterid) + return web.Response(status=204) + + @request_mapping(path="/{id}/clone", method="POST", auth_required=False) + async def http_clone(self, request): + + """ + --- + description: Brew + tags: + - FermenterRecipe + parameters: + - name: "id" + in: "path" + description: "Recipe Id" + required: true + type: "string" + - in: body + name: body + description: Recipe Data + required: false + schema: + type: object + responses: + "200": + description: successful operation + """ + id = request.match_info['id'] + data = await request.json() + return web.json_response(dict(id=await self.controller.clone(id, data.get("name")))) +