const fs = require('fs');
const brewdefs= require('./brewdefs.js');
const brewlog = require('./brewlog.js');
const Brauhaus = require('brauhaus');
let brewfather
if (brewdefs.isRaspPi()){
brewfather = require('./brewfather.js');
}else{
brewfather = require('./brewfather-no-async.js');
}
const temp = require('../brewstack/nodeDrivers/therm/temp.js');
const { debug } = require('console');
require('brauhaus-beerxml');
const FLOW_TIMEOUT_SECS = 2;
/**
* Brewing options
* @typedef {Object} brewOptions
* @property {string} filename - Name to use in logs
* @property {string} brewname -
* @property {number} strikeLitres - Initial fill of kettle
* @property {number} strikeTemp - Liquor temp pre mash
* @property {string} sparge - "batch" or "none"
* @property {number} spargeLitres - Sparge water volume.
* @property {number} spargeTemp - Sparge water temperature.
* @property {number} mashMins - Duration of the mash
* @property {number} boilMins - Duration of the boil
* @property {number} fermentTempC - Temperature to maintain during fermentation
* @property {number} fermentDays - Duration of the fermentation
* @property {number} whirlpoolMins - delay post boil, pre chill.
* @property {number} valveSwitchDelay - time allowed to verify a valve state change.
* @property {number} numBrews - Number of brews/mash cycles
*/
/**
* Json Brew data
* @desc JSON representation of a brew
* @typedef {Object} jsonData
* @property {string} brewname - Name to use in logs
* @property {number} bottleLitres - Final botttling volume
* @property {number} grainTempC
* @property {number} grainKg
* @property {number} mashTempC
* @property {string} sparge - "batch" or "none"
* @property {number} spargeTempC - Sparge water temperature.
* @property {number} mashMins - Duration of the mash
* @property {number} boilMins - Duration of the boil
* @property {number} fermentTempC - Temperature to maintain during fermentation
* @property {number} fermentDays - Duration of the fermentation
*/
const simOptions = {
simulate :!brewdefs.isRaspPi(),
ambientTemp :10,
speedupFactor :6,
resetLog :true //Clear out logs before starting
}
/**
* @desc Calculate the strike volume for a single infusion mash.
* @param {number} grainMass - Dry grain mass in Kg.
* @returns {number} - Strike litres
*/
let strikeVolume = grainMass => grainMass * brewdefs.WATER_TO_GRIST + brewdefs.MASHTUN_LOSSES
/**
* @desc Calculate the strike temperature.
* @param {number} gristTemp - Dry grain temperature.
* @param {number} mashTemp - Desired mash temperature.
* @param {number} strikeVolume - Volume of water.
* @param {number} mashVolume - grainMass.
* @returns {number} strikeTemp
*/
const strikeTemp = (gristTemp, mashTemp, strikeVolume, mashVolume) => {
const grainHeatCapacity = 0.4;
const R = (strikeVolume/mashVolume); //weight of water to grist
const pipeOutTemp = mashTemp + ((grainHeatCapacity/R) * (mashTemp - gristTemp));
const stainlessSteelHeatCapacity = .502;
const pipeMass = 1;
const pipeTemp = gristTemp;
const strike = pipeOutTemp + (pipeMass * stainlessSteelHeatCapacity * (pipeOutTemp - pipeTemp));
return strike;
}
const BREW_ROOT = "./brews/";
function validateJSON(jsonOptions){
let result = true;
result = result && (jsonOptions.grainKg > 0);
result = result && (jsonOptions.boilMins > 0);
result = result && (jsonOptions.bottleLitres > 0);
result = result && ((jsonOptions.sparge == "none") || (jsonOptions.sparge == "batch"));
result = result && (jsonOptions.brewname.length > 0);
result = result && (jsonOptions.mashTemp > 0);
result = result && (jsonOptions.mashTempC > 0);
result = result && (jsonOptions.mashMins > 0);
result = result && (jsonOptions.boilMins > 0);
result = result && (jsonOptions.fermentTempC > 0);
result = result && (jsonOptions.fermentDays > 0);
return result;
}
function readJSONSync(filename, speedupFactor){
simOptions.speedupFactor = speedupFactor;
const jsonFilename = `${BREW_ROOT + filename}/${filename}.json`;
let jsonOptions = JSON.parse(fs.readFileSync(jsonFilename).toString());
if (validateJSON(jsonOptions)){
let strikeLitres;
if (jsonOptions.sparge === "batch"){
strikeLitres = strikeVolume(jsonOptions.grainKg);
}else{
strikeLitres = 0;
}
let spargeLitres;
let spargeTempC;
let grainAbsorption = jsonOptions.grainKg;
let evaporationLoss = (brewdefs.EVAP_RATE_L_PER_HOUR / 60) * jsonOptions.boilMins;
let losses = grainAbsorption
+ evaporationLoss
+ brewdefs.PIPE_LOSSES
+ brewdefs.TRUB_LOSSES;
spargeLitres = jsonOptions.bottleLitres - strikeLitres + losses;
if (jsonOptions.sparge == "none"){
strikeLitres += spargeLitres;
spargeLitres = 0;
}else
if (jsonOptions.sparge == "batch"){
spargeTempC = jsonOptions.spargeTempC;
}
let options = {
filename,
totalWeight: jsonOptions.grainKg,
brewname: jsonOptions.brewname,
strikeLitres,
sparge: jsonOptions.sparge,
spargeLitres,
strikeTemp: jsonOptions.mashTemp,
spargeTemp: spargeTempC,
mashMins: jsonOptions.mashMins,
boilMins: jsonOptions.boilMins,
fermentTempC: jsonOptions.fermentTempC,
fermentDays: jsonOptions.fermentDays,
flowTimeoutSecs:FLOW_TIMEOUT_SECS,
flowReportSecs: 1,
whirlpoolMins: 20,
valveSwitchDelay: 5000,
numBrews:1
};
options.sim = simOptions;
if (simOptions.simulate){
options.boilMins = options.boilMins / simOptions.speedupFactor;
options.mashMins = options.mashMins / simOptions.speedupFactor;
options.whirlpoolMins = options.whirlpoolMins / simOptions.speedupFactor;
options.fermentDays = options.fermentDays / simOptions.speedupFactor;
options.flowTimeoutSecs = options.flowTimeoutSecs / simOptions.speedupFactor;
options.flowReportSecs = options.flowReportSecs / simOptions.speedupFactor;
options.valveSwitchDelay = options.valveSwitchDelay / simOptions.speedupFactor;
}
options.mashTempC = jsonOptions.mashTempC;
return options;
}
}
function readBrewfatherJSONSync(filename, speedupFactor){
//"mashWaterAmount": 16.66,
//"spargeWaterAmount": 19.93,
const jsonFilename = `${BREW_ROOT + filename}/${filename}.json`;
const brewfather = JSON.parse(fs.readFileSync(jsonFilename).toString());
let recipe;
if (brewfather._type === 'batch'){
recipe = brewfather.recipe;
} else if (brewfather._type === 'recipe'){
recipe = brewfather;
}else{
console.error("Unknown file type=",brewfather._type)
console.assert(false);
}
return brewfatherOptions(recipe, filename, speedupFactor);
}
function readBrewfatherJSON(speedupFactor, cb){
return new Promise((resolve,reject) => {
console.log("readBrewfatherJSON");
brewfather.currentRecipe(recipe => {
const filename = recipe.name;
const options = cb(brewfatherOptions(recipe, filename, speedupFactor));
resolve(options);
});
});
}
function brewfatherOptions(recipe, filename, speedupFactor){
simOptions.speedupFactor = speedupFactor;
let strikeLitres;
let options = {
filename,
brewname: recipe.name,
strikeLitres: recipe.data.mashWaterAmount,
strikeTemp: recipe.data.strikeTemp,
sparge: recipe.data.spargeWaterAmount > 0,
spargeLitres: recipe.data.hltWaterAmount,
spargeTemp: recipe.data.strikeTemp,
mashMins: recipe.mash.steps.reduce((prev, {stepTime}) => prev + stepTime,0),
boilMins: recipe.boilTime,
fermentTempC: recipe.fermentation.steps[0].stepTemp,
fermentDays: recipe.fermentation.steps[0].stepTime,
fermentSteps: recipe.fermentation.steps,
mashSteps: recipe.mash.steps.map(({stepTime, stepTemp}) => ({
mins:stepTime,
temp:stepTemp
})),
flowTimeoutSecs:FLOW_TIMEOUT_SECS,
flowReportSecs: 1,
whirlpoolMins: recipe.equipment.whirlpoolTime,
valveSwitchDelay: 5000,
numBrews:1
};
options.sim = simOptions;
if (simOptions.simulate){
options.boilMins = options.boilMins / simOptions.speedupFactor;
options.mashMins = options.mashMins / simOptions.speedupFactor;
options.whirlpoolMins = options.whirlpoolMins / simOptions.speedupFactor;
options.fermentDays = options.fermentDays / simOptions.speedupFactor;
options.flowTimeoutSecs = options.flowTimeoutSecs / simOptions.speedupFactor;
options.flowReportSecs = options.flowReportSecs / simOptions.speedupFactor;
options.valveSwitchDelay = options.valveSwitchDelay / simOptions.speedupFactor;
options.mashSteps = options.mashSteps.map(step => ({mins:step.mins / simOptions.speedupFactor ,temp:step.temp}));
}
options.mashTempC = recipe.mash.steps[0].stepTemp;
return options;
}
module.exports = {
readJSONSync,
brewfatherJSON(speedupFactor) {
return new Promise((resolve, reject) => {
readBrewfatherJSON(speedupFactor, options => {
temp.start(options)
.then(() => {
resolve(options);
});
});
});
},
/**
* @param {string} filename - Name of the brew.
* @returns {Promise} brewOptions brewOptions -
*/
readJSON(filename, speedupFactor) {
let options = readBrewfatherJSONSync(filename, speedupFactor);
brewlog.startSync(options);
return new Promise((resolve, reject) => {
temp.start(brewlog.startSync(options))
.then(() => {
resolve(options);
});
});
},
/**
* @param {string} filename - Name of the brew.
* @returns {brewOptions} brewOptions -
*/
readXML(filename, speedupFactor=10) {
simOptions.speedupFactor = speedupFactor;
let sparge = "none";
const xmlFilename = `${BREW_ROOT + filename}/${filename}.xml`;
if (!fs.existsSync(xmlFilename)){
return {};
}
const beerxml = fs.readFileSync(xmlFilename, {encoding:'utf8'});
// Get a list of recipes
const recipes = Brauhaus.Recipe.fromBeerXml(beerxml);
const r = recipes[0];
r.calculate();
let totalWeight = 0;
r.fermentables.forEach(({weight}) => {
totalWeight += weight;
});
const grainAbsorption = totalWeight *0.61; //factor taken from BeerSmith
let strikeTempC;
let mashMins;
let spargeLitres = 0;
let strikeLitres;
let totalLitres = r.boilSize + grainAbsorption;//total volume for no-sparge
spargeLitres = 0;
strikeTempC = r.mash.spargeTemp;
mashMins = 90;
const spargeTempC = r.mash.spargeTemp;
let mashSteps = [];
if (r.mash.steps.length > 0){
strikeTempC = strikeTemp(r.mash.grainTemp, r.mash.steps[0].temp, totalLitres, totalWeight);
} else {
strikeTempC = r.mash.spargeTemp;
}
r.mash.steps.forEach(step => {
if (step.name === 'Mash In'){
mashMins = step.time;
strikeLitres = strikeVolume(totalWeight);
spargeLitres = totalLitres - strikeLitres;
sparge = "batch";
if (/*sparge === none*/true){
sparge = "none";
strikeLitres = totalLitres;
}
}
else if (step.name === 'Saccharification'){
mashMins = step.time;
strikeLitres = r.boilSize + grainAbsorption;
sparge = "none";
}
else if (step.name === 'Mash Out'){
if (sparge === "batch"){
//modify volumes
strikeLitres = strikeVolume(totalWeight);
spargeLitres = totalLitres - strikeLitres;
}
// }else if (step.name === 'Mash Step'){//Infusoin step
// if (sparge === "none"){
// //modify volumes
// strikeLitres = totalLitres;
// spargeLitres = 0;
// }
// mashSteps.push({mins:step.time, temp:step.temp});
}else if (step.type === 'Infusion'){//Infusoin step
if (sparge === "none"){
console.log({totalLitres})
//modify volumes
strikeLitres = totalLitres;
spargeLitres = 0;
}
mashSteps.push({mins:step.time, temp:step.temp});
}else{
console.log("ERROR: Unknown mash type=",step.type);
return {};
}
});
let options = {
filename,
brewname: r.name,
strikeLitres,
strikeTemp: strikeTempC,
spargeTemp: spargeTempC,
fermentTempC: r.primaryTemp,
fermentDays: r.primaryDays,
flowTimeoutSecs:FLOW_TIMEOUT_SECS,
flowReportSecs: 1,
mashMins,
numBrews :1,
sparge,
spargeLitres,
mashSteps,
boilMins: 90,//this is missing from r
whirlpoolMins: 15,
valveSwitchDelay: 5000
};
options.sim = simOptions;
if (simOptions.simulate){
options.flowTimeoutSecs = options.flowTimeoutSecs / simOptions.speedupFactor;
options.boilMins = options.boilMins / simOptions.speedupFactor;
options.whirlpoolMins = options.whirlpoolMins / simOptions.speedupFactor;
options.valveSwitchDelay = options.valveSwitchDelay / simOptions.speedupFactor;
options.mashMins = options.mashMins / simOptions.speedupFactor;
options.fermentDays = options.fermentDays / simOptions.speedupFactor;
options.mashSteps = options.mashSteps.map(step=>({temp:step.temp, mins:step.mins / simOptions.speedupFactor}));
}else{
simOptions.speedupFactor = 1;
}
return options;
},
//These are used only when there is no brew data from json or xml files.
defaultOptions() {
let options = {
ambientTemp: 10,
flowTimeoutSecs: FLOW_TIMEOUT_SECS,
flowReportSecs: 1,
whirlpoolMins: 5,
boilMins: 90,
valveSwitchDelay: 5000,
sparge: 'batch',
spargeLitres: 20
}
simOptions.speedupFactor = 10;
options.sim = simOptions;
if (simOptions.simulate){
options.flowTimeoutSecs = options.flowTimeoutSecs / simOptions.speedupFactor;
options.boilMins = options.boilMins / simOptions.speedupFactor;
options.whirlpoolMins = options.whirlpoolMins / simOptions.speedupFactor;
options.valveSwitchDelay = options.valveSwitchDelay / simOptions.speedupFactor;
options.mashMins = options.mashMins / simOptions.speedupFactor;
options.fermentDays = options.fermentDays / simOptions.speedupFactor;
}
return options;
}
}