Преглед на файлове

- Recipe Book

- BugFixes
- Refactoring
core_refactoring
Manuel83 преди 8 години
родител
ревизия
13836a5680
променени са 17 файла, в които са добавени 660 реда и са изтрити 63 реда
  1. +4
    -0
      modules/base_plugins/actor.py
  2. +2
    -2
      modules/base_plugins/sensor.py
  3. +221
    -1
      modules/base_plugins/steps.py
  4. +21
    -0
      modules/core/basetypes.py
  5. +10
    -1
      modules/core/core.py
  6. +2
    -1
      modules/database/dbmodel.py
  7. +10
    -4
      modules/fermenter/__init__.py
  8. +11
    -0
      modules/kettle/__init__.py
  9. +1
    -0
      modules/plugin/__init__.py
  10. +146
    -0
      modules/recipe_book/__init__.py
  11. +9
    -10
      modules/recipe_import/beerxml.py
  12. +1
    -0
      modules/step/__init__.py
  13. +44
    -44
      modules/ui/static/bundle.js
  14. +4
    -0
      name.py
  15. +0
    -0
      recipes/empty
  16. +1
    -0
      run.py
  17. +173
    -0
      upload/beer.xml

+ 4
- 0
modules/base_plugins/actor.py Целия файл

@@ -8,6 +8,7 @@ class Dummy(Actor):

@cbpi.addon.actor.action("WOHOO")
def myaction(self):
print "WOOHOO"
pass

def on(self, power=100):
@@ -29,6 +30,7 @@ class MyController(KettleController):
def run(self):
while self.is_running():


self.sleep(1)

@cbpi.addon.fermenter.controller()
@@ -38,6 +40,8 @@ class MyController2(FermenterController):
def run(self):
while self.is_running():
print "HALLO"

self.get_target_temp()
self.sleep(1)

@cbpi.addon.core.initializer(order=200)


+ 2
- 2
modules/base_plugins/sensor.py Целия файл

@@ -24,7 +24,7 @@ class Dummy(Sensor):
@cbpi.addon.sensor.action("WOHOO")
def myaction(self):

print "SENSOR ACTION HALLO!!!"
self.api.notify(headline="WOHOO", message="HALLO")

def execute(self):
while True:
@@ -32,7 +32,7 @@ class Dummy(Sensor):
self.update_value(int(self.text))
except:
pass
self.api.sleep(1)
self.api.sleep(5)

@cbpi.addon.core.action(key="clear", label="Clear all Logs")
def woohoo(cbpi):


+ 221
- 1
modules/base_plugins/steps.py Целия файл

@@ -1,6 +1,7 @@
from modules.core.basetypes import Step
from modules.core.core import cbpi
from modules.core.proptypes import Property
import time


@cbpi.addon.step.type("Dummy Step")
@@ -22,4 +23,223 @@ class Dummy(Step):

def reset(self):

self.stop_timer()
self.stop_timer()



# -*- coding: utf-8 -*-


@cbpi.addon.step.type("Dummy Step")
class MashStep(Step):
'''
Just put the decorator @cbpi.step on top of a method
'''
# Properties
temp = Property.Number("Temperature", configurable=True, description="Target Temperature of Mash Step")
kettle = Property.Kettle("Kettle", description="Kettle in which the mashing takes place")
timer = Property.Number("Timer in Minutes", configurable=True, description="Timer is started when the target temperature is reached")

def init(self):
'''
Initialize Step. This method is called once at the beginning of the step
:return:
'''
# set target tep
self.set_target_temp(self.temp, self.kettle)

@cbpi.addon.step.action("Start Timer")
def start(self):
'''
Custom Action which can be execute form the brewing dashboard.
All method with decorator @cbpi.action("YOUR CUSTOM NAME") will be available in the user interface
:return:
'''
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)

def reset(self):
self.stop_timer()
self.set_target_temp(self.temp, self.kettle)

def finish(self):
self.set_target_temp(0, self.kettle)

def execute(self):
'''
This method is execute in an interval
:return:
'''

# Check if Target Temp is reached
if self.get_kettle_temp(self.kettle) >= float(self.temp):
# Check if Timer is Running
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)

# Check if timer finished and go to next step
if self.is_timer_finished() == True:
self.notify("Mash Step Completed!", "Starting the next step", timeout=None)
self.next()


@cbpi.addon.step.type("MashInStep")
class MashInStep(Step):
'''
Just put the decorator @cbpi.step on top of a method
'''
# Properties
temp = Property.Number("Temperature", configurable=True, description="Target Temperature of Mash Step")
kettle = Property.Kettle("Kettle", description="Kettle in which the mashing takes place")
s = False


def init(self):
'''
Initialize Step. This method is called once at the beginning of the step
:return:
'''
# set target tep
self.s = False
self.set_target_temp(self.temp, self.kettle)



def execute(self):
'''
This method is execute in an interval
:return:
'''

# Check if Target Temp is reached
if self.get_kettle_temp(self.kettle) >= float(self.temp) and self.s is False:
self.s = True
self.notify("Step Temp Reached!", "Please press the next button to continue", timeout=None)



@cbpi.addon.step.type("MashInStep")
class ChilStep(Step):

timer = Property.Number("Timer in Minutes", configurable=True, default_value=0, description="Timer is started immediately")

@cbpi.addon.step.action("Start Timer")
def start(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)

def reset(self):
self.stop_timer()


def finish(self):
pass

def execute(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)

if self.is_timer_finished() == True:
self.next()

@cbpi.addon.step.type("MashInStep")
class PumpStep(Step):

pump = Property.Actor("Pump", description="Pump actor gets toogled")
timer = Property.Number("Timer in Minutes", configurable=True, default_value=0, description="Timer is started immediately")

@cbpi.addon.step.action("Start Timer")
def start(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)

def reset(self):
self.stop_timer()


def finish(self):
self.actor_off(int(self.pump))

def init(self):
self.actor_on(int(self.pump))

def execute(self):
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)

if self.is_timer_finished() == True:
self.next()

@cbpi.addon.step.type("MashInStep")
class BoilStep(Step):
'''
Just put the decorator @cbpi.step on top of a method
'''
# Properties
temp = Property.Number("Temperature", configurable=True, default_value=100, description="Target temperature for boiling")
kettle = Property.Kettle("Kettle", description="Kettle in which the boiling step takes place")
timer = Property.Number("Timer in Minutes", configurable=True, default_value=90, description="Timer is started when target temperature is reached")
hop_1 = Property.Number("Hop 1 Addition", configurable=True, description="Fist Hop alert")
hop_1_added = Property.Number("",default_value=None)
hop_2 = Property.Number("Hop 2 Addition", configurable=True, description="Second Hop alert")
hop_2_added = Property.Number("", default_value=None)
hop_3 = Property.Number("Hop 3 Addition", configurable=True)
hop_3_added = Property.Number("", default_value=None, description="Third Hop alert")
hop_4 = Property.Number("Hop 4 Addition", configurable=True)
hop_4_added = Property.Number("", default_value=None, description="Fourth Hop alert")
hop_5 = Property.Number("Hop 5 Addition", configurable=True)
hop_5_added = Property.Number("", default_value=None, description="Fives Hop alert")

def init(self):
'''
Initialize Step. This method is called once at the beginning of the step
:return:
'''
# set target tep
self.set_target_temp(self.temp, self.kettle)

@cbpi.addon.step.action("Start Timer")
def start(self):
'''
Custom Action which can be execute form the brewing dashboard.
All method with decorator @cbpi.action("YOUR CUSTOM NAME") will be available in the user interface
:return:
'''
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)

def reset(self):
self.stop_timer()
self.set_target_temp(self.temp, self.kettle)

def finish(self):
self.set_target_temp(0, self.kettle)


def check_hop_timer(self, number, value):

if self.__getattribute__("hop_%s_added" % number) is not True and time.time() > (
self.timer_end - (int(self.timer) * 60 - int(value) * 60)):
self.__setattr__("hop_%s_added" % number, True)
self.notify("Hop Alert", "Please add Hop %s" % number, timeout=None)

def execute(self):
'''
This method is execute in an interval
:return:
'''
# Check if Target Temp is reached
if self.get_kettle_temp(self.kettle) >= float(self.temp):
# Check if Timer is Running
if self.is_timer_finished() is None:
self.start_timer(int(self.timer) * 60)
else:
self.check_hop_timer(1, self.hop_1)
self.check_hop_timer(2, self.hop_2)
self.check_hop_timer(3, self.hop_3)
self.check_hop_timer(4, self.hop_4)
self.check_hop_timer(5, self.hop_5)
# Check if timer finished and go to next step
if self.is_timer_finished() == True:
self.notify("Boil Step Completed!", "Starting the next step", timeout=None)
self.next()

+ 21
- 0
modules/core/basetypes.py Целия файл

@@ -227,6 +227,8 @@ class Timer(object):
return False
class Step(Base, Timer):
@@ -267,6 +269,25 @@ class Step(Base, Timer):
self.value = None
self.__dirty = False
def set_target_temp(self, temp, id=None):
temp = float(temp)
try:
if id is None:
self.api.emit("SET_TARGET_TEMP", id=self.kettle_id, temp=temp)
else:
self.api.emit("SET_TARGET_TEMP", id=id, temp=temp)
except Exception as e:
self.api.notify("Faild to set Target Temp", "", type="warning")
def get_kettle_temp(self, id=None):
id = int(id)
if id is None:
id = self.kettle_id
return self.api.sensor.get_value(int(self.api.cache.get("kettle").get(id).sensor))
def is_dirty(self):
return self.__dirty


+ 10
- 1
modules/core/core.py Целия файл

@@ -364,13 +364,22 @@ class CraftBeerPI(object):
method.callback = True
self.cache[key] = method
def get_config_parameter(self, key, default):
def get_config_parameter(self, key, default=None):
cfg = self.cache["config"].get(key)
if cfg is None:
return default
else:
return cfg.value
def set_config_parameter(self, name, value):
from modules.config import Config
with self._app.app_context():
update_data = {"name": name, "value": value}
self.cache.get("config")[name].__dict__.update(**update_data)
c = Config.update(**update_data)
self.ws_emit("UPDATE_CONFIG", c)
def emit(self, key, **kwargs):
if self.eventbus.get(key) is not None:


+ 2
- 1
modules/database/dbmodel.py Целия файл

@@ -131,4 +131,5 @@ class FermenterStep(DBModel):
def reset_all_steps(cls,id):
cur = get_db().cursor()
cur.execute("UPDATE %s SET state = 'I', start = NULL, end = NULL, timer_start = NULL WHERE fermenter_id = ?" % cls.__table_name__, (id,))
get_db().commit()
get_db().commit()


+ 10
- 4
modules/fermenter/__init__.py Целия файл

@@ -106,6 +106,7 @@ class FermenterView(BaseView):

@route('/<int:id>/start', methods=['POST'])
def start_fermentation(self, id):
print "START"
active = None
for idx, s in enumerate(cbpi.cache.get(self.cache_key)[id].steps):
if s.state == 'A':
@@ -196,6 +197,7 @@ class FermenterView(BaseView):
return cbpi.cache["fermenter"].get(id)

def target_temp_reached(self,id, step):
print "TARGET TEMP REACHED"
timestamp = time.time()

days = step.days * 24 * 60 * 60
@@ -210,19 +212,22 @@ class FermenterView(BaseView):
cbpi.ws_emit("UPDATE_FERMENTER", cbpi.cache.get(self.cache_key)[id])

def check_step(self):
print "CHECK STEP"
print cbpi.cache["fermenter_task"]
for key, value in cbpi.cache["fermenter_task"].iteritems():

print value
try:
fermenter = self.get_fermenter(key)
current_temp = current_temp = cbpi.get_sensor_value(int(fermenter.sensor))
current_temp = current_temp = cbpi.sensor.get_value(int(fermenter.sensor))

if value.timer_start is None:
print "TIMER IS NONE"
if value.direction == "H" :
print "TIMER WATING FOR HEATING"
if current_temp >= value.temp:
self.target_temp_reached(key,value)
else:
print "TIMER WATING FOR COILING"
if current_temp <= value.temp:
self.target_temp_reached(key, value)
else:
@@ -231,6 +236,7 @@ class FermenterView(BaseView):
else:
pass
except Exception as e:
print e
pass




+ 11
- 0
modules/kettle/__init__.py Целия файл

@@ -217,6 +217,17 @@ class KettleView(BaseView):
self.api.brewing.toggle_automatic(id)
return ('', 204)
@cbpi.addon.core.listen("SET_TARGET_TEMP")
def set_target_temp(id, temp):
'''
Change Taget Temp Event
:param id: kettle id
:param temp: target temp to set
:return: None
'''
KettleView().postTargetTemp(id,temp)
@cbpi.addon.core.backgroundjob(key="read_target_temps", interval=5)
def read_target_temps(api):
"""


+ 1
- 0
modules/plugin/__init__.py Целия файл

@@ -46,6 +46,7 @@ class PluginView(FlaskView):
response = requests.get("https://raw.githubusercontent.com/Manuel83/craftbeerpi-plugins/master/plugins.yaml")
self.api.cache["plugins"] = self.merge(yaml.load(response.text), self.api.cache["plugins"])
for key, value in cbpi.cache["plugins"].iteritems():
print key
value["installed"] = os.path.isdir("./plugins/%s/" % (key))
return json.dumps(cbpi.cache["plugins"])


+ 146
- 0
modules/recipe_book/__init__.py Целия файл

@@ -0,0 +1,146 @@
import os
import re
import time

import datetime
from flask import json, request, send_from_directory
from flask_classy import route, FlaskView

from modules.core.db import DBModel
from modules.core.baseview import BaseView
from modules.core.core import cbpi
from modules.database.dbmodel import Step
from yaml import Loader, Dumper
from yaml import load, dump

from modules.step import StepView


class RecipeBook(FlaskView):


@route('/load', methods=["POST"])
def load(self):

data = request.json
recipe_name = data.get("name")
if re.match("^[A-Za-z0-9_-]*$", recipe_name) is None:
return ('Recipie Name contains not allowed characters', 500)

with open("./recipes/%s.json" % recipe_name) as json_data:
d = json.load(json_data)
Step.delete_all()
StepView().reset()
for s in d["steps"]:
Step.insert(**{"name": s.get("name"), "type": s.get("type"), "config": s.get("config")})

self.api.ws_emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % recipe_name, message="")

return ('', 204)

@route('/download/<name>', methods=["GET"])
def download(self, name):

"""
Download a log file by name
---
tags:
- logs
parameters:
- in: path
name: file
schema:
type: string
required: true
description: filename
responses:
200:
description: Log file downloaded
"""
file = "%s.json" % name
print file
if not self.check_filename(file):
return ('File Not Found111', 404)
return send_from_directory('../../recipes', file, as_attachment=True, attachment_filename=file)

def check_filename(self, name):
import re
print "CHECK"
pattern = re.compile('^([A-Za-z0-9-_])+.json$')
return True if pattern.match(name) else False

@route('/<name>', methods=["DELETE"])
def remove(self, name):


recipe_name = name
if re.match("^[A-Za-z0-9_-]*$", recipe_name) is None:
return ('Recipie Name contains not allowed characters', 500)

filename = "./recipes/%s.json" % recipe_name
if os.path.isfile(filename) == True:
os.remove(filename)
self.api.notify(headline="Recipe %s deleted successfully" % recipe_name, message="")
return ('', 204)
else:
self.api.notify(headline="Faild to delete Recipe %s deleted" % recipe_name, message="")
return ('', 404)

return ('', 204)


@route('/', methods=["GET"])
def get_all(self):
result = []
for filename in os.listdir("./recipes"):
if filename.endswith(".json"):

result.append({"id":filename.split(".")[0], "name": filename.split(".")[0], "change_date": self._modification_date('./recipes/%s' % filename)})
return json.dumps(result)


def _modification_date(self, filename):
t = os.path.getmtime(filename)
return datetime.datetime.fromtimestamp(t).strftime('%Y-%m-%d %H:%M:%S')

@route('/save', methods=["POST"])
def save(self):
"""
Save Recepie
---
tags:
- steps
responses:
204:
description: Recipe saved
"""



recipe_name = self.api.get_config_parameter("brew_name")

if recipe_name is None or len(recipe_name) <= 0:
self.api.notify(headline="Please set brew name", message="Recipe not saved!", type="danger")
return ('Recipie Name contains not allowed characters', 500)

if re.match("^[\sA-Za-z0-9_-]*$", recipe_name) is None:
self.api.notify(headline="Only alphanummeric charaters are allowd for recipe name", message="", type="danger")
return ('Recipie Name contains not allowed characters', 500)

recipe_data = {"name": recipe_name, "steps": Step.get_all()}

file_name = recipe_name.replace(" ", "_")
with open('./recipes/%s.json' % file_name, 'w') as outfile:
json.dump(recipe_data, outfile, indent=4)

self.api.notify(headline="Recipe %s saved successfully" % recipe_name, message="")


return ('', 204)


@cbpi.addon.core.initializer(order=2000)
def init(cbpi):
RecipeBook.api = cbpi
RecipeBook.register(cbpi._app, route_base='/api/recipebook')

+ 9
- 10
modules/recipe_import/beerxml.py Целия файл

@@ -55,11 +55,12 @@ class BeerXMLImport(FlaskView):
if request.method == 'POST':
file = request.files['file']
if file and self.allowed_file(file.filename):
file.save(os.path.join(self.api.app.config['UPLOAD_FOLDER'], "beer.xml"))
file.save(os.path.join(self.api.get_config_parameter('UPLOAD_FOLDER', "./upload"), "beer.xml"))
self.api.notify(headline="Upload Successful", message="The Beer XML file was uploaded succesfully")
return ('', 204)
return ('', 404)
except Exception as e:
print e
self.api.notify(headline="Upload Failed", message="Failed to upload Beer xml", type="danger")
return ('', 500)

@@ -88,12 +89,12 @@ class BeerXMLImport(FlaskView):
name = self.getRecipeName(id)
self.api.set_config_parameter("brew_name", name)
boil_time = self.getBoilTime(id)
mashstep_type = cbpi.get_config_parameter("step_mash", "MashStep")
mash_kettle = cbpi.get_config_parameter("step_mash_kettle", None)
mashstep_type = self.api.get_config_parameter("step_mash", "MashStep")
mash_kettle = self.api.get_config_parameter("step_mash_kettle", None)

boilstep_type = cbpi.get_config_parameter("step_boil", "BoilStep")
boil_kettle = cbpi.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if cbpi.get_config_parameter("unit", "C") == "C" else 212
boilstep_type = self.api.get_config_parameter("step_boil", "BoilStep")
boil_kettle = self.api.get_config_parameter("step_boil_kettle", None)
boil_temp = 100 if self.api.get_config_parameter("unit", "C") == "C" else 212

# READ KBH DATABASE
Step.delete_all()
@@ -108,7 +109,8 @@ class BeerXMLImport(FlaskView):
Step.insert(**{"name": "Boil", "type": boilstep_type, "config": {"kettle": boil_kettle, "temp": boil_temp, "timer": boil_time}})
## Add Whirlpool step
Step.insert(**{"name": "Whirlpool", "type": "ChilStep", "config": {"timer": 15}})
self.api.emit("UPDATE_ALL_STEPS", Step.get_all())

self.api.ws_emit("UPDATE_ALL_STEPS", Step.get_all())
self.api.notify(headline="Recipe %s loaded successfully" % name, message="")
except Exception as e:
self.api.notify(headline="Failed to load Recipe", message=e.message, type="danger")
@@ -125,9 +127,6 @@ class BeerXMLImport(FlaskView):
return float(e.find('./RECIPE[%s]/BOIL_TIME' % (str(id))).text)

def getSteps(self, id):



e = xml.etree.ElementTree.parse(self.BEER_XML_FILE).getroot()
steps = []
for e in e.findall('./RECIPE[%s]/MASH/MASH_STEPS/MASH_STEP' % (str(id))):


+ 1
- 0
modules/step/__init__.py Целия файл

@@ -43,6 +43,7 @@ class StepView(BaseView):
"""
self.model.delete_all()
self.api.emit("ALL_BREWING_STEPS_DELETED")
self.api.set_config_parameter("brew_name", "")
cbpi.ws_emit("UPDATE_ALL_STEPS", self.model.get_all())
return ('', 204)


+ 44
- 44
modules/ui/static/bundle.js
Файловите разлики са ограничени, защото са твърде много
Целия файл


+ 4
- 0
name.py Целия файл

@@ -0,0 +1,4 @@
import re

recipe_name = "Manuel 8881881 18181 "
print re.match("^[\sA-Za-z0-9_-]*$", recipe_name)

+ 0
- 0
recipes/empty Целия файл


+ 1
- 0
run.py Целия файл

@@ -29,5 +29,6 @@ from modules.base_plugins.steps import *
from modules.example_plugins.WebViewJquery import *
from modules.example_plugins.WebViewReactJs import *
from modules.example_plugins.swagger import *
from modules.recipe_book import *

cbpi.run()

+ 173
- 0
upload/beer.xml Целия файл

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<RECIPES>
<RECIPE>
<NAME>Pale Ale</NAME>
<VERSION>1</VERSION>
<TYPE>All Grain</TYPE>
<BREWER></BREWER>
<DISPLAY_BATCH_SIZE>6.5 gal</DISPLAY_BATCH_SIZE>
<DISPLAY_BOIL_SIZE>7.5 gal</DISPLAY_BOIL_SIZE>
<BATCH_SIZE>24.60517657</BATCH_SIZE>
<BOIL_SIZE>28.39058835</BOIL_SIZE>
<BOIL_TIME>60</BOIL_TIME>
<EFFICIENCY>63</EFFICIENCY>
<NOTES>&#13;
</NOTES>
<PRIMARY_TEMP>20</PRIMARY_TEMP>
<EST_COLOR>8.75</EST_COLOR>
<IBU>62.22</IBU>
<IBU_METHOD>Tinseth</IBU_METHOD>
<EST_ABV>4.88</EST_ABV>
<EST_OG>1.047 sg</EST_OG>
<EST_FG>1.01 sg</EST_FG>
<OG>1.047</OG>
<FG>1.01</FG>
<PRIMING_SUGAR_NAME></PRIMING_SUGAR_NAME>
<CARBONATION_USED></CARBONATION_USED>
<BF_PRIMING_METHOD></BF_PRIMING_METHOD>
<BF_PRIMING_AMOUNT></BF_PRIMING_AMOUNT>
<BF_CO2_LEVEL></BF_CO2_LEVEL>
<BF_CO2_UNIT>Volumes</BF_CO2_UNIT>
<URL></URL>
<BATCH_SIZE_MODE>f</BATCH_SIZE_MODE>
<YEAST_STARTER>false</YEAST_STARTER>
<NO_CHILL_EXTRA_MINUTES></NO_CHILL_EXTRA_MINUTES>
<STARTING_MASH_THICKNESS>3.33880656</STARTING_MASH_THICKNESS>
<PITCH_RATE>0.35</PITCH_RATE>
<FERMENTABLES>
<FERMENTABLE>
<NAME>Pale 2-Row</NAME>
<VERSION>1</VERSION>
<TYPE>Grain</TYPE>
<AMOUNT>4.08233133</AMOUNT>
<YIELD>80.43</YIELD>
<COLOR>1.8</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN>American</ORIGIN>
</FERMENTABLE>
<FERMENTABLE>
<NAME>Caramel / Crystal 60L</NAME>
<VERSION>1</VERSION>
<TYPE>Grain</TYPE>
<AMOUNT>0.45359237</AMOUNT>
<YIELD>73.91</YIELD>
<COLOR>60</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN>American</ORIGIN>
</FERMENTABLE>
<FERMENTABLE>
<NAME>Rye</NAME>
<VERSION>1</VERSION>
<TYPE>Grain</TYPE>
<AMOUNT>1.0205828325</AMOUNT>
<YIELD>82.61</YIELD>
<COLOR>3.5</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN>American</ORIGIN>
</FERMENTABLE>
<FERMENTABLE>
<NAME>Flaked Wheat</NAME>
<VERSION>1</VERSION>
<TYPE>Adjunct</TYPE>
<AMOUNT>0.226796185</AMOUNT>
<YIELD>73.91</YIELD>
<COLOR>2</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN></ORIGIN>
</FERMENTABLE>
<FERMENTABLE>
<NAME>Carapils</NAME>
<VERSION>1</VERSION>
<TYPE>Grain</TYPE>
<AMOUNT>0.226796185</AMOUNT>
<YIELD>76.09</YIELD>
<COLOR>1.3</COLOR>
<ADD_AFTER_BOIL>false</ADD_AFTER_BOIL>
<ORIGIN>German</ORIGIN>
</FERMENTABLE>
</FERMENTABLES>
<HOPS>
<HOP>
<NAME>El Dorado</NAME>
<VERSION>1</VERSION>
<ALPHA>15.7</ALPHA>
<AMOUNT>0.0283495231</AMOUNT>
<USE>First Wort</USE>
<USER_HOP_USE>First Wort</USER_HOP_USE>
<TIME>0</TIME>
<FORM>Leaf</FORM>
</HOP>
<HOP>
<NAME>El Dorado</NAME>
<VERSION>1</VERSION>
<ALPHA>15.7</ALPHA>
<AMOUNT>0.0283495231</AMOUNT>
<USE>Boil</USE>
<USER_HOP_USE>Boil</USER_HOP_USE>
<TIME>30</TIME>
<FORM>Leaf</FORM>
</HOP>
<HOP>
<NAME>El Dorado</NAME>
<VERSION>1</VERSION>
<ALPHA>15.7</ALPHA>
<AMOUNT>0.0283495231</AMOUNT>
<USE>Dry Hop</USE>
<USER_HOP_USE>Dry Hop</USER_HOP_USE>
<TIME>14400</TIME>
<FORM>Leaf</FORM>
</HOP>
</HOPS>
<MISCS/>
<MASH>
<NAME>Mash Steps</NAME>
<VERSION>1</VERSION>
<GRAIN_TEMP>20</GRAIN_TEMP>
<MASH_STEPS>
<MASH_STEP>
<NAME>STEP1</NAME>
<VERSION>1</VERSION>
<TYPE>Infusion</TYPE>
<STEP_TIME>60</STEP_TIME>
<INFUSE_AMOUNT>1.5141647136</INFUSE_AMOUNT>
<STEP_TEMP>65.555555555556</STEP_TEMP>
</MASH_STEP>
</MASH_STEPS>
</MASH>
<YEASTS>
<YEAST>
<NAME>California Ale Yeast WLP001</NAME>
<VERSION>1</VERSION>
<TYPE>Ale</TYPE>
<FORM>Liquid</FORM>
<AMOUNT>0.1</AMOUNT>
<PRODUCT_ID>WLP001</PRODUCT_ID>
<LABORATORY>White Labs</LABORATORY>
<ATTENUATION>76.5</ATTENUATION>
<FLOCCULATION>Medium</FLOCCULATION>
<MIN_TEMPERATURE>20</MIN_TEMPERATURE>
<MAX_TEMPERATURE>22.777777777778</MAX_TEMPERATURE>
</YEAST>
</YEASTS>
<WATERS/>
<STYLE>
<NAME>English IPA</NAME>
<VERSION>1</VERSION>
<CATEGORY>India Pale Ale (IPA)</CATEGORY>
<CATEGORY_NUMBER>14</CATEGORY_NUMBER>
<STYLE_LETTER>A</STYLE_LETTER>
<STYLE_GUIDE>BJCP</STYLE_GUIDE>
<TYPE>Ale</TYPE>
<OG_MIN>1.05</OG_MIN>
<OG_MAX>1.075</OG_MAX>
<FG_MIN>1.01</FG_MIN>
<FG_MAX>1.018</FG_MAX>
<ABV_MIN>5</ABV_MIN>
<ABV_MAX>7.5</ABV_MAX>
<IBU_MIN>40</IBU_MIN>
<IBU_MAX>60</IBU_MAX>
<COLOR_MIN>8</COLOR_MIN>
<COLOR_MAX>14</COLOR_MAX>
</STYLE>
</RECIPE>
</RECIPES>

Loading…
Отказ
Запис