mirror of
https://github.com/PiBrewing/craftbeerpi4.git
synced 2024-11-09 17:07:43 +01:00
Native Brewfather recipe import added
This commit is contained in:
parent
fcb2400240
commit
a36591636e
3 changed files with 295 additions and 4 deletions
|
@ -27,6 +27,20 @@
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"value": ""
|
"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": {
|
"TEMP_UNIT": {
|
||||||
"description": "Temperature Unit",
|
"description": "Temperature Unit",
|
||||||
"name": "TEMP_UNIT",
|
"name": "TEMP_UNIT",
|
||||||
|
|
|
@ -12,7 +12,6 @@ from cbpi.controller.kettle_controller import KettleController
|
||||||
from cbpi.api.base import CBPiBase
|
from cbpi.api.base import CBPiBase
|
||||||
from cbpi.api.config import ConfigType
|
from cbpi.api.config import ConfigType
|
||||||
import webbrowser
|
import webbrowser
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os.path
|
import os.path
|
||||||
from os import listdir
|
from os import listdir
|
||||||
|
@ -21,8 +20,8 @@ import json
|
||||||
import shortuuid
|
import shortuuid
|
||||||
import yaml
|
import yaml
|
||||||
from ..api.step import StepMove, StepResult, StepState
|
from ..api.step import StepMove, StepResult, StepState
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import base64
|
||||||
|
|
||||||
class UploadController:
|
class UploadController:
|
||||||
|
|
||||||
|
@ -59,6 +58,41 @@ class UploadController:
|
||||||
except:
|
except:
|
||||||
return []
|
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):
|
def get_creation_path(self):
|
||||||
creation_path = self.cbpi.config.get("RECIPE_CREATION_PATH", "upload")
|
creation_path = self.cbpi.config.get("RECIPE_CREATION_PATH", "upload")
|
||||||
path = {'path': 'upload'} if creation_path == '' else {'path': creation_path}
|
path = {'path': 'upload'} if creation_path == '' else {'path': creation_path}
|
||||||
|
@ -191,14 +225,21 @@ class UploadController:
|
||||||
AutoNext = "No"
|
AutoNext = "No"
|
||||||
await self.create_step(step_type, step_name, step_kettle, step_timer, step_temp, AutoMode, sensor, Notification, AutoNext)
|
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,))
|
c.execute('SELECT Kochdauer FROM Sud WHERE ID = ?', (Recipe_ID,))
|
||||||
row = c.fetchone()
|
row = c.fetchone()
|
||||||
step_time = str(int(row[0]))
|
step_time = str(int(row[0]))
|
||||||
|
|
||||||
|
logging.info("Boil Time: {}".format(step_time))
|
||||||
|
|
||||||
FirstWortFlag = self.getFirstWortKBH(Recipe_ID)
|
FirstWortFlag = self.getFirstWortKBH(Recipe_ID)
|
||||||
|
|
||||||
|
logging.info(FirstWortFlag)
|
||||||
|
|
||||||
BoilTimeAlerts = self.getBoilAlertsKBH(Recipe_ID)
|
BoilTimeAlerts = self.getBoilAlertsKBH(Recipe_ID)
|
||||||
|
|
||||||
|
logging.info(BoilTimeAlerts)
|
||||||
|
|
||||||
step_kettle = self.id
|
step_kettle = self.id
|
||||||
step_type = self.boil if self.boil != "" else "BoilStep"
|
step_type = self.boil if self.boil != "" else "BoilStep"
|
||||||
step_name = "Boil Step"
|
step_name = "Boil Step"
|
||||||
|
@ -216,6 +257,8 @@ class UploadController:
|
||||||
Hop5 = str(int(BoilTimeAlerts[4])) if len(BoilTimeAlerts) >= 5 else None
|
Hop5 = str(int(BoilTimeAlerts[4])) if len(BoilTimeAlerts) >= 5 else None
|
||||||
Hop6 = str(int(BoilTimeAlerts[5])) if len(BoilTimeAlerts) >= 6 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)
|
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
|
# Add Waitstep as Whirlpool
|
||||||
|
@ -505,6 +548,200 @@ class UploadController:
|
||||||
|
|
||||||
return steps
|
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
|
# 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=""):
|
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,
|
step_string = { "name": name,
|
||||||
|
@ -535,5 +772,4 @@ class UploadController:
|
||||||
async with aiohttp.ClientSession(headers=headers) as session:
|
async with aiohttp.ClientSession(headers=headers) as session:
|
||||||
async with session.post(self.url, data=step) as response:
|
async with session.post(self.url, data=step) as response:
|
||||||
return await response.text()
|
return await response.text()
|
||||||
await self.push_update()
|
await self.push_update()
|
||||||
|
|
||||||
|
|
|
@ -101,6 +101,47 @@ class UploadHttpEndpoints():
|
||||||
await self.controller.xml_recipe_creation(xml_id['id'])
|
await self.controller.xml_recipe_creation(xml_id['id'])
|
||||||
return web.Response(status=200)
|
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)
|
@request_mapping(path="/getpath", auth_required=False)
|
||||||
async def http_getpath(self, request):
|
async def http_getpath(self, request):
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue