Merge pull request #35 from avollkopf/development

Merge update from Development Branch
This commit is contained in:
Alexander Vollkopf 2022-01-28 18:15:49 +01:00 committed by GitHub
commit 4f82dc85b3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 353 additions and 20 deletions

View file

@ -1,2 +1 @@
__version__ = "4.0.1.2" __version__ = "4.0.1.7"

View file

@ -10,6 +10,7 @@ from cbpi.utils.utils import load_config
from zipfile import ZipFile from zipfile import ZipFile
from cbpi.craftbeerpi import CraftBeerPi from cbpi.craftbeerpi import CraftBeerPi
import os import os
import platform
import pathlib import pathlib
import shutil import shutil
import yaml import yaml
@ -152,12 +153,17 @@ def check_for_setup():
if zip_content == True: if zip_content == True:
print("Found correct content. Starting Restore process") print("Found correct content. Starting Restore process")
output_path = pathlib.Path(os.path.join(".", 'config')) output_path = pathlib.Path(os.path.join(".", 'config'))
system = platform.system()
print(system)
if system != "Windows":
owner = output_path.owner() owner = output_path.owner()
group = output_path.group() group = output_path.group()
print("Removing old config folder") print("Removing old config folder")
shutil.rmtree(output_path, ignore_errors=True) shutil.rmtree(output_path, ignore_errors=True)
print("Extracting zip file to config folder") print("Extracting zip file to config folder")
zip.extractall(output_path) zip.extractall(output_path)
zip.close()
if system != "Windows":
print("Changing owner and group of config folder recursively to {}:{}".format(owner,group)) print("Changing owner and group of config folder recursively to {}:{}".format(owner,group))
recursive_chown(output_path, owner, group) recursive_chown(output_path, owner, group)
print("Removing backup file") print("Removing backup file")

View file

@ -59,6 +59,16 @@ class UploadController:
except: except:
return [] return []
async def get_json_recipes(self):
try:
path = os.path.join(".", 'config', "upload", "mmum.json")
e = json.load(open(path))
result =[]
result.append({'value': str(1), 'label': e['Name']})
return result
except:
return []
async def get_brewfather_recipes(self,offset=0): async def get_brewfather_recipes(self,offset=0):
brewfather = True brewfather = True
result=[] result=[]
@ -123,6 +133,20 @@ class UploadController:
self.cbpi.notify("Error" "XML Recipe upload failed: {}".format(e), NotificationType.ERROR) self.cbpi.notify("Error" "XML Recipe upload failed: {}".format(e), NotificationType.ERROR)
pass pass
elif content_type == 'application/json':
try:
mmum_json = recipe_file.read().decode('utf-8','replace')
if recipe_file and self.allowed_file(filename, 'json'):
self.path = os.path.join(".", 'config', "upload", "mmum.json")
f = open(self.path, "w")
f.write(mmum_json)
f.close()
self.cbpi.notify("Success", "JSON Recipe {} has been uploaded".format(filename), NotificationType.SUCCESS)
except Exception as e:
self.cbpi.notify("Error" "JSON Recipe upload failed: {}".format(e), NotificationType.ERROR)
pass
elif content_type == 'application/octet-stream': elif content_type == 'application/octet-stream':
try: try:
content = recipe_file.read() content = recipe_file.read()
@ -293,6 +317,235 @@ class UploadController:
else: else:
self.cbpi.notify('Recipe Upload', 'No default Kettle defined. Please specify default Kettle in settings', NotificationType.ERROR) self.cbpi.notify('Recipe Upload', 'No default Kettle defined. Please specify default Kettle in settings', NotificationType.ERROR)
def findMax(self, string):
self.path = os.path.join(".", 'config', "upload", "mmum.json")
e = json.load(open(self.path))
for idx in range(1,20):
search_string = string.replace("%%",str(idx))
i = idx
if search_string not in e:
break
return i
def getJsonMashin(self, id):
self.path = os.path.join(".", 'config', "upload", "mmum.json")
e = json.load(open(self.path))
return float(e['Infusion_Einmaischtemperatur'])
async def json_recipe_creation(self, Recipe_ID):
config = self.get_config_values()
if self.kettle is not None:
# load mmum-json file located in upload folder
self.path = os.path.join(".", 'config', "upload", "mmum.json")
if os.path.exists(self.path) is False:
self.cbpi.notify("File Not Found", "Please upload a MMuM-JSON File", NotificationType.ERROR)
e = json.load(open(self.path))
name = e['Name']
boil_time = float(e['Kochzeit_Wuerze'])
await self.create_recipe(name)
hops = []
for idx in range(1,self.findMax("Hopfen_%%_Kochzeit")):
hops_name = "%sg %s %s%% alpha" % (e["Hopfen_{}_Menge".format(idx)],e["Hopfen_{}_Sorte".format(idx)],e["Hopfen_{}_alpha".format(idx)])
if e["Hopfen_{}_Kochzeit".format(idx)].isnumeric():
if boil_time is not e["Hopfen_{}_Kochzeit".format(idx)].isnumeric():
alert = float(e["Hopfen_{}_Kochzeit".format(idx)])
elif e["Hopfen_{}_Kochzeit".format(idx)] == "Whirlpool":
alert = float(1)
else:
self.api.notify(headline="No Number at Hoptime", message="Please change json-File at Hopfen_{}_Kochzeit".format(idx), type="danger")
alert = float(1)
hops.append({"name":hops_name,"time":alert})
firstHops=[]
for idx in range(1,self.findMax("Hopfen_VWH_%%_Sorte")):
firstHops_name = "%sg %s %s%% alpha" % (e["Hopfen_VWH_{}_Menge".format(idx)],e["Hopfen_VWH_{}_Sorte".format(idx)],e["Hopfen_VWH_{}_alpha".format(idx)])
firstHops.append({"name":firstHops_name})
FirstWort= self.getFirstWort(firstHops, "json")
miscs = []
for idx in range(1,self.findMax("WeitereZutat_Wuerze_%%_Kochzeit")):
miscs_name = "%s%s %s" % (e["WeitereZutat_Wuerze_{}_Menge".format(idx)],e["WeitereZutat_Wuerze_{}_Einheit".format(idx)],e["WeitereZutat_Wuerze_{}_Name".format(idx)])
if e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)].isnumeric():
alert = float(e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)])
elif e["WeitereZutat_Wuerze_{}_Kochzeit".format(idx)] == "Whirlpool":
alert = float(1)
else:
self.api.notify(headline="No Number at Hoptime", message="Please change json-File at WeitereZutat_Wuerze_{}_Kochzeit".format(idx), type="danger")
alert = float(1)
miscs.append({"name":miscs_name,"time":alert})
# 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
last_step_temp = 0
logging.info(step_kettle) ###################################################
for row in self.getSteps(Recipe_ID, "json"):
step_name = str(row.get("name"))
step_timer = str(int(row.get("timer")))
step_temp = str(int(row.get("temp")))
last_step_temp = step_temp
sensor = self.kettle.sensor
if MashIn_Flag == True:
if row.get("timer") == 0:
step_type = self.mashin if self.mashin != "" else "MashInStep"
Notification = "Target temperature reached. Please add malt."
MashIn_Flag = False
if step_name is None or step_name == "":
step_name = "MashIn"
elif self.addmashin == "Yes":
step_type = self.mashin if self.mashin != "" else "MashInStep"
Notification = "Target temperature reached. Please add malt."
MashIn_Flag = False
step_string = { "name": "MashIn",
"props": {
"AutoMode": self.AutoMode,
"Kettle": self.id,
"Sensor": self.kettle.sensor,
"Temp": self.getJsonMashin(Recipe_ID),
"Timer": 0,
"Notification": Notification
},
"status_text": "",
"status": "I",
"type": step_type
}
await self.create_step(step_string)
logging.info(step_kettle) ###################################################
step_type = self.mash if self.mash != "" else "MashStep"
Notification = ""
else:
step_type = self.mash if self.mash != "" else "MashStep"
Notification = ""
else:
step_type = self.mash if self.mash != "" else "MashStep"
Notification = ""
step_string = { "name": step_name,
"props": {
"AutoMode": self.AutoMode,
"Kettle": self.id,
"Sensor": self.kettle.sensor,
"Temp": step_temp,
"Timer": step_timer,
"Notification": Notification
},
"status_text": "",
"status": "I",
"type": step_type
}
await self.create_step(step_string)
# MashOut -> mashStep to reach mashout-temp for 1 min
if last_step_temp != e["Abmaischtemperatur"]:
step_string = { "name": "MashOut",
"props": {
"AutoMode": self.AutoMode,
"Kettle": self.id,
"Sensor": self.kettle.sensor,
"Temp": e["Abmaischtemperatur"],
"Timer": 1,
"Notification": ""
},
"status_text": "",
"status": "I",
"type": "MashStep"
}
await self.create_step(step_string)
# Lautering -> Simple step that sends notification and waits for user input to move to next step (AutoNext=No)
if self.mashout == "NotificationStep":
step_string = { "name": "Lautering",
"props": {
"AutoNext": "No",
"Kettle": self.id,
"Notification": "Mash Process completed. Please start lautering and press next to start boil."
},
"status_text": "",
"status": "I",
"type": self.mashout
}
await self.create_step(step_string)
# Measure Original Gravity -> Simple step that sends notification
step_string = { "name": "Measure Original Gravity",
"props": {
"AutoNext": "No",
"Kettle": self.id,
"Notification": "What is the original gravity of the beer wort?"
},
"status_text": "",
"status": "I",
"type": "NotificationStep"
}
await self.create_step(step_string)
# Boil step including hop alarms and alarm for first wort hops -> Automode is set tu yes
Hops = self.getBoilAlerts(hops, miscs, "json")
step_kettle = self.boilid
step_type = self.boil if self.boil != "" else "BoilStep"
step_time = str(int(boil_time))
step_temp = self.BoilTemp
sensor = self.boilkettle.sensor
LidAlert = "Yes"
logging.info(step_temp) ###################################################
step_string = { "name": "Boil Step",
"props": {
"AutoMode": self.AutoMode,
"Kettle": step_kettle,
"Sensor": sensor,
"Temp": step_temp,
"Timer": step_time,
"First_Wort": FirstWort,
"LidAlert": LidAlert,
"Hop_1": Hops[0],
"Hop_2": Hops[1],
"Hop_3": Hops[2],
"Hop_4": Hops[3],
"Hop_5": Hops[4],
"Hop_6": Hops[5]
},
"status_text": "",
"status": "I",
"type": step_type
}
await self.create_step(step_string)
# Measure Original Gravity -> Simple step that sends notification
step_string = { "name": "Measure Original Gravity",
"props": {
"AutoNext": "No",
"Kettle": self.id,
"Notification": "What is the original gravity of the beer wort?"
},
"status_text": "",
"status": "I",
"type": "NotificationStep"
}
await self.create_step(step_string)
await self.create_Whirlpool_Cooldown()
self.cbpi.notify('MMuM-JSON Recipe created ', name, NotificationType.INFO)
else:
self.cbpi.notify('Recipe Upload', 'No default Kettle defined. Please specify default Kettle in settings', NotificationType.ERROR)
async def xml_recipe_creation(self, Recipe_ID): async def xml_recipe_creation(self, Recipe_ID):
config = self.get_config_values() config = self.get_config_values()
@ -316,7 +569,7 @@ class UploadController:
# AutoMode is yes to start and stop automatic mode or each step # AutoMode is yes to start and stop automatic mode or each step
MashIn_Flag = True MashIn_Flag = True
step_kettle = self.id step_kettle = self.id
for row in self.getSteps(Recipe_ID): for row in self.getSteps(Recipe_ID, "xml"):
step_name = str(row.get("name")) step_name = str(row.get("name"))
step_timer = str(int(row.get("timer"))) step_timer = str(int(row.get("timer")))
step_temp = str(int(row.get("temp"))) step_temp = str(int(row.get("temp")))
@ -428,15 +681,26 @@ class UploadController:
# XML functions to retrieve xml repice parameters (if multiple recipes are stored in one xml file, id could be used) # XML functions to retrieve xml repice parameters (if multiple recipes are stored in one xml file, id could be used)
def getSteps(self, id): def getSteps(self, id, recipe_type):
e = xml.etree.ElementTree.parse(self.path).getroot()
steps = [] steps = []
if recipe_type == "xml":
e = xml.etree.ElementTree.parse(self.path).getroot()
for e in e.findall('./RECIPE[%s]/MASH/MASH_STEPS/MASH_STEP' % (str(id))): for e in e.findall('./RECIPE[%s]/MASH/MASH_STEPS/MASH_STEP' % (str(id))):
if self.cbpi.config.get("TEMP_UNIT", "C") == "C": if self.cbpi.config.get("TEMP_UNIT", "C") == "C":
temp = float(e.find("STEP_TEMP").text) temp = float(e.find("STEP_TEMP").text)
else: else:
temp = round(9.0 / 5.0 * float(e.find("STEP_TEMP").text) + 32, 2) temp = round(9.0 / 5.0 * float(e.find("STEP_TEMP").text) + 32, 2)
steps.append({"name": e.find("NAME").text, "temp": temp, "timer": float(e.find("STEP_TIME").text)}) steps.append({"name": e.find("NAME").text, "temp": temp, "timer": float(e.find("STEP_TIME").text)})
elif recipe_type == "json":
self.path = os.path.join(".", 'config', "upload", "mmum.json")
e = json.load(open(self.path))
for idx in range(1,self.findMax("Infusion_Rastzeit%%")):
if self.cbpi.config.get("TEMP_UNIT", "C") == "C":
temp = float(e["Infusion_Rasttemperatur{}".format(idx)])
else:
temp = round(9.0 / 5.0 * float(e["Infusion_Rasttemperatur{}".format(idx)]) + 32, 2)
steps.append({"name": "Rast {}".format(idx), "temp": temp, "timer": float(e["Infusion_Rastzeit{}".format(idx)])})
return steps return steps
@ -632,6 +896,9 @@ class UploadController:
alerts.append(float(hop['time'])) alerts.append(float(hop['time']))
elif recipe_type == "kbh": elif recipe_type == "kbh":
alerts.append(float(hop[0])) alerts.append(float(hop[0]))
elif recipe_type == "json":
alerts.append(float(hop['time']))
## There might also be miscelaneous additions during boild time ## There might also be miscelaneous additions during boild time
if miscs is not None: if miscs is not None:
for misc in miscs: for misc in miscs:
@ -644,6 +911,8 @@ class UploadController:
alerts.append(float(misc['time'])) alerts.append(float(misc['time']))
elif recipe_type == "kbh": elif recipe_type == "kbh":
alerts.append(float(misc[0])) alerts.append(float(misc[0]))
elif recipe_type == "json":
alerts.append(float(misc['time']))
## Dedupe and order the additions by their time, to prevent multiple alerts at the same time ## Dedupe and order the additions by their time, to prevent multiple alerts at the same time
alerts = sorted(list(set(alerts))) alerts = sorted(list(set(alerts)))
## CBP should have these additions in reverse ## CBP should have these additions in reverse
@ -672,6 +941,9 @@ class UploadController:
for hop in hops: for hop in hops:
if hop['use'] == "First Wort": if hop['use'] == "First Wort":
alert="Yes" alert="Yes"
elif recipe_type == "json":
for hop in hops:
alert="Yes"
return alert return alert
async def create_Whirlpool_Cooldown(self): async def create_Whirlpool_Cooldown(self):
@ -705,7 +977,8 @@ class UploadController:
"Kettle": self.boilid, "Kettle": self.boilid,
"Timer": step_timer, "Timer": step_timer,
"Temp": step_temp, "Temp": step_temp,
"Sensor": cooldown_sensor "Sensor": cooldown_sensor,
"Actor": self.CoolDownActor
}, },
"status_text": "", "status_text": "",
"status": "I", "status": "I",
@ -729,6 +1002,7 @@ class UploadController:
self.BoilTemp = self.cbpi.config.get("steps_boil_temp", 98) self.BoilTemp = self.cbpi.config.get("steps_boil_temp", 98)
#get default cooldown temp alarm setting #get default cooldown temp alarm setting
self.CoolDownTemp = self.cbpi.config.get("steps_cooldown_temp", 25) self.CoolDownTemp = self.cbpi.config.get("steps_cooldown_temp", 25)
self.CoolDownActor = self.cbpi.config.get("steps_cooldown_actor", None)
# get default Kettle from Settings # get default Kettle from Settings
self.id = self.cbpi.config.get('MASH_TUN', None) self.id = self.cbpi.config.get('MASH_TUN', None)
self.boilid = self.cbpi.config.get('BoilKettle', None) self.boilid = self.cbpi.config.get('BoilKettle', None)
@ -758,6 +1032,7 @@ class UploadController:
"cooldown": str(self.cooldown), "cooldown": str(self.cooldown),
"boiltemp": str(self.BoilTemp), "boiltemp": str(self.BoilTemp),
"cooldowntemp": str(self.CoolDownTemp), "cooldowntemp": str(self.CoolDownTemp),
"cooldownactor": self.CoolDownActor,
"temp_unit": str(self.TEMP_UNIT), "temp_unit": str(self.TEMP_UNIT),
"AutoMode": str(self.AutoMode) "AutoMode": str(self.AutoMode)
} }

View file

@ -28,6 +28,7 @@ class ConfigUpdate(CBPiExtension):
default_cool_temp = 20 if TEMP_UNIT == "C" else 68 default_cool_temp = 20 if TEMP_UNIT == "C" else 68
boil_temp = self.cbpi.config.get("steps_boil_temp", None) boil_temp = self.cbpi.config.get("steps_boil_temp", None)
cooldown_sensor = self.cbpi.config.get("steps_cooldown_sensor", None) cooldown_sensor = self.cbpi.config.get("steps_cooldown_sensor", None)
cooldown_actor = self.cbpi.config.get("steps_cooldown_actor", None)
cooldown_temp = self.cbpi.config.get("steps_cooldown_temp", None) cooldown_temp = self.cbpi.config.get("steps_cooldown_temp", None)
mashin_step = self.cbpi.config.get("steps_mashin", None) mashin_step = self.cbpi.config.get("steps_mashin", None)
mash_step = self.cbpi.config.get("steps_mash", None) mash_step = self.cbpi.config.get("steps_mash", None)
@ -60,6 +61,13 @@ class ConfigUpdate(CBPiExtension):
except: except:
logger.warning('Unable to update database') logger.warning('Unable to update database')
if cooldown_actor is None:
logger.info("INIT Cooldown Actor Setting")
try:
await self.cbpi.config.add("steps_cooldown_actor", "", ConfigType.ACTOR, "Actor to trigger cooldown water on and off (default: None)")
except:
logger.warning('Unable to update database')
if cooldown_temp is None: if cooldown_temp is None:
logger.info("INIT Cooldown Temp Setting") logger.info("INIT Cooldown Temp Setting")
try: try:

View file

@ -41,7 +41,7 @@ class ReadThread (threading.Thread):
try: try:
if self.sensor_name is None: if self.sensor_name is None:
return return
with open('/sys/bus/w1/devices/w1_bus_master1/%s/w1_slave' % self.sensor_name, 'r') as content_file: with open('/sys/bus/w1/devices/%s/w1_slave' % self.sensor_name, 'r') as content_file:
content = content_file.read() content = content_file.read()
if (content.split('\n')[0].split(' ')[11] == "YES"): if (content.split('\n')[0].split(' ')[11] == "YES"):
temp = float(content.split("=")[-1]) / 1000 # temp in Celcius temp = float(content.split("=")[-1]) / 1000 # temp in Celcius

View file

@ -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='/json', method="GET", auth_required=False)
async def get_json_list(self, request):
"""
---
description: Get recipe list from json file
tags:
- Upload
responses:
"200":
description: successful operation
"""
json_list = await self.controller.get_json_recipes()
return web.json_response(json_list)
@request_mapping(path='/json', method="POST", auth_required=False)
async def create_json_recipe(self, request):
"""
---
description: Create recipe from json file with selected id
tags:
- Upload
parameters:
- name: "id"
in: "body"
description: "Recipe ID: {'id': ID}"
required: true
type: "string"
responses:
"200":
description: successful operation
"""
json_id = await request.json()
await self.controller.json_recipe_creation(json_id['id'])
return web.Response(status=200)
@request_mapping(path='/bf/{offset}/', method="POST", auth_required=False) @request_mapping(path='/bf/{offset}/', method="POST", auth_required=False)
async def get_bf_list(self, request): async def get_bf_list(self, request):
""" """

View file

@ -32,7 +32,11 @@ class CBPiWebSocket:
self.logger.debug("broadcast to ws clients. Data: %s" % data) self.logger.debug("broadcast to ws clients. Data: %s" % data)
for ws in self._clients: for ws in self._clients:
async def send_data(ws, data): async def send_data(ws, data):
try:
await ws.send_json(data=data, dumps=json_dumps) await ws.send_json(data=data, dumps=json_dumps)
except Exception as e:
self.logger.error("Error with client %s: %s" % (ws, str(e)))
self.cbpi.app.loop.create_task(send_data(ws, data)) self.cbpi.app.loop.create_task(send_data(ws, data))
async def websocket_handler(self, request): async def websocket_handler(self, request):