Native Brewfather recipe import added

This commit is contained in:
avollkopf 2021-06-13 14:38:18 +02:00
parent fcb2400240
commit a36591636e
3 changed files with 295 additions and 4 deletions

View file

@ -27,6 +27,20 @@
"type": "string",
"value": ""
},
"brewfather_api_key": {
"description": "Brewfather API Kay",
"name": "brewfather_api_key",
"options": null,
"type": "string",
"value": ""
},
"brewfather_user_id": {
"description": "Brewfather User ID",
"name": "brewfather_user_id",
"options": null,
"type": "string",
"value": ""
},
"TEMP_UNIT": {
"description": "Temperature Unit",
"name": "TEMP_UNIT",

View file

@ -12,7 +12,6 @@ from cbpi.controller.kettle_controller import KettleController
from cbpi.api.base import CBPiBase
from cbpi.api.config import ConfigType
import webbrowser
import logging
import os.path
from os import listdir
@ -21,8 +20,8 @@ import json
import shortuuid
import yaml
from ..api.step import StepMove, StepResult, StepState
import re
import base64
class UploadController:
@ -59,6 +58,41 @@ class UploadController:
except:
return []
async def get_brewfather_recipes(self):
brewfather = True
result=[]
self.url="https://api.brewfather.app/v1/recipes"
brewfather_user_id = self.cbpi.config.get("brewfather_user_id", None)
if brewfather_user_id == "" or brewfather_user_id is None:
brewfather = False
brewfather_api_key = self.cbpi.config.get("brewfather_api_key", None)
if brewfather_api_key == "" or brewfather_api_key is None:
brewfather = False
if brewfather == True:
encodedData = base64.b64encode(bytes(f"{brewfather_user_id}:{brewfather_api_key}", "ISO-8859-1")).decode("ascii")
headers={"Authorization": "Basic %s" % encodedData}
async with aiohttp.ClientSession(headers=headers) as bf_session:
async with bf_session.get(self.url) as r:
bf_recipe_list = await r.json()
await bf_session.close()
if bf_recipe_list:
for row in bf_recipe_list:
recipe_id = row['_id']
name = row['name']
element = {'value': recipe_id, 'label': name}
result.append(element)
return result
else:
return []
else:
return []
def get_creation_path(self):
creation_path = self.cbpi.config.get("RECIPE_CREATION_PATH", "upload")
path = {'path': 'upload'} if creation_path == '' else {'path': creation_path}
@ -191,14 +225,21 @@ class UploadController:
AutoNext = "No"
await self.create_step(step_type, step_name, step_kettle, step_timer, step_temp, AutoMode, sensor, Notification, AutoNext)
c.execute('SELECT Kochdauer FROM Sud WHERE ID = ?', (Recipe_ID,))
row = c.fetchone()
step_time = str(int(row[0]))
logging.info("Boil Time: {}".format(step_time))
FirstWortFlag = self.getFirstWortKBH(Recipe_ID)
logging.info(FirstWortFlag)
BoilTimeAlerts = self.getBoilAlertsKBH(Recipe_ID)
logging.info(BoilTimeAlerts)
step_kettle = self.id
step_type = self.boil if self.boil != "" else "BoilStep"
step_name = "Boil Step"
@ -216,6 +257,8 @@ class UploadController:
Hop5 = str(int(BoilTimeAlerts[4])) if len(BoilTimeAlerts) >= 5 else None
Hop6 = str(int(BoilTimeAlerts[5])) if len(BoilTimeAlerts) >= 6 else None
await self.create_step(step_type, step_name, step_kettle, step_time, step_temp, AutoMode, sensor, Notification, AutoNext, LidAlert, FirstWort, Hop1, Hop2, Hop3, Hop4, Hop5, Hop6)
# Add Waitstep as Whirlpool
@ -505,6 +548,200 @@ class UploadController:
return steps
async def bf_recipe_creation(self, Recipe_ID):
self.kettle = None
#Define MashSteps
self.mashin = self.cbpi.config.get("steps_mashin", "MashStep")
self.mash = self.cbpi.config.get("steps_mash", "MashStep")
self.mashout = self.cbpi.config.get("steps_mashout", None) # Currently used only for the Braumeister
self.boil = self.cbpi.config.get("steps_boil", "BoilStep")
self.cooldown = self.cbpi.config.get("steps_cooldown", "WaitStep")
#get default boil temp from settings
self.BoilTemp = self.cbpi.config.get("steps_boil_temp", 98)
#get default cooldown temp alarm setting
self.CoolDownTemp = self.cbpi.config.get("steps_cooldown_temp", 25)
#get server port from settings and define url for api calls -> adding steps
self.port = str(self.cbpi.static_config.get('port',8000))
self.url="http://127.0.0.1:" + self.port + "/step2/"
self.TEMP_UNIT=self.cbpi.config.get("TEMP_UNIT", "C")
# get default Kettle from Settings
self.id = self.cbpi.config.get('MASH_TUN', None)
try:
self.kettle = self.cbpi.kettle.find_by_id(self.id)
except:
self.cbpi.notify('Recipe Upload', 'No default Kettle defined. Please specify default Kettle in settings', NotificationType.ERROR)
if self.id is not None or self.id != '':
brewfather = True
result=[]
self.bf_url="https://api.brewfather.app/v1/recipes/" + Recipe_ID
brewfather_user_id = self.cbpi.config.get("brewfather_user_id", None)
if brewfather_user_id == "" or brewfather_user_id is None:
brewfather = False
brewfather_api_key = self.cbpi.config.get("brewfather_api_key", None)
if brewfather_api_key == "" or brewfather_api_key is None:
brewfather = False
if brewfather == True:
encodedData = base64.b64encode(bytes(f"{brewfather_user_id}:{brewfather_api_key}", "ISO-8859-1")).decode("ascii")
headers={"Authorization": "Basic %s" % encodedData}
bf_recipe = ""
async with aiohttp.ClientSession(headers=headers) as bf_session:
async with bf_session.get(self.bf_url) as r:
bf_recipe = await r.json()
await bf_session.close()
if bf_recipe !="":
RecipeName = bf_recipe['name']
BoilTime = bf_recipe['boilTime']
mash_steps=bf_recipe['mash']['steps']
hops=bf_recipe['hops']
try:
miscs = bf_recipe['miscs']
except:
miscs = None
FirstWort = "No"
for hop in hops:
if hop['use'] == "First Wort":
FirstWort="Yes"
# Create recipe in recipe Book with name of first recipe in xml file
self.recipeID = await self.cbpi.recipe.create(RecipeName)
# send recipe to mash profile
await self.cbpi.recipe.brew(self.recipeID)
# remove empty recipe from recipe book
await self.cbpi.recipe.remove(self.recipeID)
# Mash Steps -> first step is different as it heats up to defined temp and stops with notification to add malt
# AutoMode is yes to start and stop automatic mode or each step
MashIn_Flag = True
step_kettle = self.id
for step in mash_steps:
step_name = step['name']
step_timer = str(int(step['stepTime']))
if self.TEMP_UNIT == "C":
step_temp = str(int(step['stepTemp']))
else:
step_temp = str(round((9.0 / 5.0 * int(step['stepTemp']) + 32)))
sensor = self.kettle.sensor
if MashIn_Flag == True and int(step_timer) == 0:
step_type = self.mashin if self.mashin != "" else "MashInStep"
AutoMode = "Yes" if step_type == "MashInStep" else "No"
Notification = "Target temperature reached. Please add malt."
MashIn_Flag = False
else:
step_type = self.mash if self.mash != "" else "MashStep"
AutoMode = "Yes" if step_type == "MashStep" else "No"
Notification = ""
await self.create_step(step_type, step_name, step_kettle, step_timer, step_temp, AutoMode, sensor, Notification)
# MashOut -> Simple step that sends notification and waits for user input to move to next step (AutoNext=No)
if self.mashout == "NotificationStep":
step_kettle = self.id
step_type = self.mashout
step_name = "Lautering"
step_timer = ""
step_temp = ""
AutoMode = ""
sensor = ""
Notification = "Mash Process completed. Please start lautering and press next to start boil."
AutoNext = "No"
await self.create_step(step_type, step_name, step_kettle, step_timer, step_temp, AutoMode, sensor, Notification, AutoNext)
# Boil step including hop alarms and alarm for first wort hops -> Automode is set tu yes
self.BoilTimeAlerts = self.getBoilAlertsBF(hops,miscs)
step_kettle = self.id
step_time = str(int(BoilTime))
step_type = self.boil if self.boil != "" else "BoilStep"
step_name = "Boil Step"
step_temp = self.BoilTemp
AutoMode = "Yes" if step_type == "BoilStep" else "No"
sensor = self.kettle.sensor
Notification = ""
AutoNext = ""
LidAlert = "Yes"
Hop1 = str(int(self.BoilTimeAlerts[0])) if len(self.BoilTimeAlerts) >= 1 else None
Hop2 = str(int(self.BoilTimeAlerts[1])) if len(self.BoilTimeAlerts) >= 2 else None
Hop3 = str(int(self.BoilTimeAlerts[2])) if len(self.BoilTimeAlerts) >= 3 else None
Hop4 = str(int(self.BoilTimeAlerts[3])) if len(self.BoilTimeAlerts) >= 4 else None
Hop5 = str(int(self.BoilTimeAlerts[4])) if len(self.BoilTimeAlerts) >= 5 else None
Hop6 = str(int(self.BoilTimeAlerts[5])) if len(self.BoilTimeAlerts) >= 6 else None
await self.create_step(step_type, step_name, step_kettle, step_time, step_temp, AutoMode, sensor, Notification, AutoNext, LidAlert, FirstWort, Hop1, Hop2, Hop3, Hop4, Hop5, Hop6)
# Add Waitstep as Whirlpool
if self.cooldown != "WaiStep" and self.cooldown !="":
step_type = "WaitStep"
step_name = "Whirlpool"
cooldown_sensor = ""
step_timer = "15"
step_temp = ""
AutoMode = ""
await self.create_step(step_type, step_name, step_kettle, step_timer, step_temp, AutoMode, cooldown_sensor)
# CoolDown step is sending a notification when cooldowntemp is reached
step_type = self.cooldown if self.cooldown != "" else "WaitStep"
step_name = "CoolDown"
cooldown_sensor = ""
step_timer = "15"
step_temp = ""
AutoMode = ""
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
step_kettle = self.id
step_timer = ""
step_temp = self.CoolDownTemp
await self.create_step(step_type, step_name, step_kettle, step_timer, step_temp, AutoMode, cooldown_sensor)
self.cbpi.notify('Brewfather App Recipe created: ', RecipeName, NotificationType.INFO)
def getBoilAlertsBF(self, hops, miscs):
alerts = []
for hop in hops:
use = hop['use']
## Hops which are not used in the boil step should not cause alerts
if use != 'Aroma' and use != 'Boil':
continue
alerts.append(float(hop['time']))
#There might also be miscelaneous additions during boild time
if miscs is not None:
for misc in miscs:
use = misc['use']
if use != 'Aroma' and use != 'Boil':
continue
alerts.append(float(misc['time']))
## Dedupe and order the additions by their time, to prevent multiple alerts at the same time
alerts = sorted(list(set(alerts)))
## CBP should have these additions in reverse
alerts.reverse()
return alerts
# function to create json to be send to api to add a step to the current mash profile. Currently all properties are send to each step which does not cuase an issue
async def create_step(self, type, name, kettle, timer, temp, AutoMode, sensor, Notification = "", AutoNext = "", LidAlert = "", FirstWort = "", Hop1 = "", Hop2 = "", Hop3 = "", Hop4 = "", Hop5 = "", Hop6=""):
step_string = { "name": name,
@ -535,5 +772,4 @@ class UploadController:
async with aiohttp.ClientSession(headers=headers) as session:
async with session.post(self.url, data=step) as response:
return await response.text()
await self.push_update()
await self.push_update()

View file

@ -101,6 +101,47 @@ class UploadHttpEndpoints():
await self.controller.xml_recipe_creation(xml_id['id'])
return web.Response(status=200)
@request_mapping(path='/bf', method="GET", auth_required=False)
async def get_bf_list(self, request):
"""
---
description: Get recipe list from Brewfather App
tags:
- Upload
responses:
"200":
description: successful operation
"""
bf_list = await self.controller.get_brewfather_recipes()
return web.json_response(bf_list)
@request_mapping(path='/bf', method="POST", auth_required=False)
async def create_bf_recipe(self, request):
"""
---
description: Create recipe from Brewfather Web App with selected id
tags:
- Upload
parameters:
- name: "id"
in: "body"
description: "Recipe ID: {'id': ID}"
required: true
type: "string"
responses:
"200":
description: successful operation
"""
bf_id = await request.json()
await self.controller.bf_recipe_creation(bf_id['id'])
return web.Response(status=200)
@request_mapping(path="/getpath", auth_required=False)
async def http_getpath(self, request):