Fri, 13 Jan 2023 12:00:58 +0100
Added a button to automatic recreate the yeast starter steps. Some code cleanup
/** * EditProduct.cpp is part of bmsapp. * * Tab 9, brewday. * * bmsapp is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * bmsapp is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * @brief Check the state by examining the date values. * 1. startdate and enddate invalid, planning/wait status. * The enddate cannot be set. * 2. startdate valid and endate invalid, brewdate is planned. * The fase will be brew. Enable setting of enddate. * 3. startdate valid, enddate and start and endtime can be set. * The enddate cannot be before the startdate and not after 4 * days from the start. * 4. startdate and enddate and times are set and valid. Block * the startdate setting. But only after setting a lot of * brewdata and acknowledge set the fase to primary. * */ void EditProduct::updateBrewday() { setStage(); qDebug() << "updateBrewday" << product->brew_date_start << product->brew_date_end; ui->brew_startDate->setDate(product->brew_date_start.date()); ui->brew_startTime->setTime(product->brew_date_start.time()); ui->brew_endDate->setDate(product->brew_date_end.date()); ui->brew_endTime->setTime(product->brew_date_end.time()); } void EditProduct::brew_date_clear() { product->brew_date_start.setDate(QDate()); ui->brew_startDate->setDate(QDate()); } void EditProduct::brew_date_today() { product->brew_date_start.setDate(QDate::currentDate()); ui->brew_startDate->setDate(QDate::currentDate()); } void EditProduct::brew_start_date_changed(QDate val) { product->brew_date_start.setDate(ui->brew_startDate->nullDate()); updateBrewday(); is_changed(); } void EditProduct::brew_end_today() // Not really, the brew start date is used. { product->brew_date_end.setDate(product->brew_date_start.date()); ui->brew_endDate->setDate(product->brew_date_end.date()); } void EditProduct::brew_end_date_changed(QDate val) { product->brew_date_end.setDate(ui->brew_endDate->nullDate()); updateBrewday(); is_changed(); } void EditProduct::brew_start_time_changed(QTime val) { product->brew_date_start.setTime(ui->brew_startTime->time()); updateBrewday(); is_changed(); } void EditProduct::brew_end_time_changed(QTime val) { product->brew_date_end.setTime(ui->brew_endTime->time()); updateBrewday(); is_changed(); } void EditProduct::brew_date_ack() { int rc = QMessageBox::warning(this, tr("Confirm brew"), tr("Confirm that the brew date and time are correct"), QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (rc == QMessageBox::No) return; product->stage = PROD_STAGE_PRIMARY; setStage(); is_changed(); } void EditProduct::brew_mashph_changed(double val) { if (product->brew_mash_ph == 0) { product->brew_mash_ph = 4.8; const QSignalBlocker blocker1(ui->brew_mashphEdit); ui->brew_mashphEdit->setValue(4.8); } else { product->brew_mash_ph = val; } is_changed(); } void EditProduct::brew_mashsg_changed(double val) { product->brew_mash_sg = val; double c = Utils::sg_to_plato(product->est_mash_sg); double m = Utils::sg_to_plato(val); if (c > 0.5) product->brew_mash_efficiency = 100 * m / c; else product->brew_mash_efficiency = 0; ui->brew_masheffShow->setValue(product->brew_mash_efficiency); is_changed(); } void EditProduct::brew_spargeph_changed(double val) { if (product->brew_sparge_ph == 0) { product->brew_sparge_ph = 4.8; const QSignalBlocker blocker1(ui->brew_spargephEdit); ui->brew_spargephEdit->setValue(4.8); } else { product->brew_sparge_ph = val; } is_changed(); } void EditProduct::brew_volume_calc(double volume, double kettle_volume, double kettle_height, double est_volume, bool aboil) { double k_cm; if (volume > 0) k_cm = Utils::kettle_cm(volume, kettle_volume, kettle_height); else k_cm = Utils::kettle_cm(est_volume, kettle_volume, kettle_height); QDialog* dialog = new QDialog(this); dialog->resize(320, 110); QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setObjectName(QString::fromUtf8("buttonBox")); buttonBox->setGeometry(QRect(40, 65, 240, 32)); buttonBox->setLayoutDirection(Qt::LeftToRight); buttonBox->setOrientation(Qt::Horizontal); buttonBox->setStandardButtons(QDialogButtonBox::Ok); buttonBox->setCenterButtons(true); QLabel *cmLabel = new QLabel(dialog); cmLabel->setObjectName(QString::fromUtf8("cmLabel")); cmLabel->setText(tr("Distance from top:")); cmLabel->setGeometry(QRect(10, 20, 141, 20)); cmLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); QDoubleSpinBox *cmEdit = new QDoubleSpinBox(dialog); cmEdit->setObjectName(QString::fromUtf8("cmEdit")); cmEdit->setGeometry(QRect(160, 20, 121, 24)); cmEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); cmEdit->setAccelerated(true); cmEdit->setDecimals(1); cmEdit->setSuffix(tr(" cm")); cmEdit->setMaximum(kettle_height * 100); cmEdit->setSingleStep(0.1); cmEdit->setValue(k_cm); if (aboil) connect(cmEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::brew_aboil_cm_changed); else connect(cmEdit, QOverload<double>::of(&QDoubleSpinBox::valueChanged), this, &EditProduct::brew_preboil_cm_changed); connect(buttonBox, SIGNAL(rejected()), dialog, SLOT(reject())); connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept())); dialog->setModal(true); dialog->exec(); disconnect(cmEdit, nullptr, nullptr, nullptr); } void EditProduct::brew_preboilph_changed(double val) { if (product->brew_preboil_ph == 0) { product->brew_preboil_ph = 4.8; const QSignalBlocker blocker1(ui->brew_preboilphEdit); ui->brew_preboilphEdit->setValue(4.8); } else { product->brew_preboil_ph = val; } is_changed(); } void EditProduct::calcEfficiencyBeforeBoil() { double m = 0; double tot; double result = 0; if (product->fermentables.size() == 0) return; // no fermentables loaded yet for (int i = 0; i < product->fermentables.size(); i++) { if (product->fermentables.at(i).added == FERMENTABLE_ADDED_MASH) { m += product->fermentables.at(i).amount * (product->fermentables.at(i).yield / 100) * (1 - product->fermentables.at(i).moisture / 100); } } tot = Utils::sg_to_plato(product->brew_preboil_sg) * (product->brew_preboil_volume / 1.04) * product->brew_preboil_sg * 10 / 1000; if (m > 0) result = round((tot / m * 100) * 10.0) / 10.0; if (result < 0) result = 0; ui->brew_preboileffShow->setValue(result); } void EditProduct::brew_preboilsg_changed(double val) { if (product->brew_preboil_sg == 0) { product->brew_preboil_sg = product->preboil_sg; const QSignalBlocker blocker1(ui->brew_preboilsgEdit); ui->brew_preboilsgEdit->setValue(product->preboil_sg); } else { product->brew_preboil_sg = val; } is_changed(); calcEfficiencyBeforeBoil(); } void EditProduct::brew_preboilvol_changed(double val) { if (product->brew_preboil_volume == 0) { product->brew_preboil_volume = product->boil_size * 1.04; const QSignalBlocker blocker1(ui->brew_preboilvolEdit); ui->brew_preboilvolEdit->setValue(product->boil_size * 1.04); } else { product->brew_preboil_volume = val; } is_changed(); calcEfficiencyBeforeBoil(); } void EditProduct::brew_preboil_cm_changed(double val) { double vol = Utils::kettle_vol(val, product->eq_kettle_volume, product->eq_kettle_height); product->brew_preboil_volume = vol; const QSignalBlocker blocker1(ui->brew_preboilvolEdit); ui->brew_preboilvolEdit->setValue(vol); is_changed(); calcEfficiencyBeforeBoil(); } void EditProduct::brew_preboil_button() { brew_volume_calc(product->brew_preboil_volume, product->eq_kettle_volume, product->eq_kettle_height, product->boil_size * 1.04, false); } void EditProduct::brew_aboilph_changed(double val) { if (product->brew_aboil_ph == 0) { product->brew_aboil_ph = 4.8; const QSignalBlocker blocker1(ui->brew_aboilphEdit); ui->brew_aboilphEdit->setValue(4.8); } else { product->brew_aboil_ph = val; } is_changed(); } void EditProduct::calcEfficiencyAfterBoil() { double m = 0; // Sugars added at mash double b = 0; // Sugars added at boil double result = 0; if (product->fermentables.size() == 0) return; // no fermentables loaded yet for (int i = 0; i < product->fermentables.size(); i++) { if (product->fermentables.at(i).added == FERMENTABLE_ADDED_MASH) { m += product->fermentables.at(i).amount * (product->fermentables.at(i).yield / 100) * (1 - product->fermentables.at(i).moisture / 100); } else if (product->fermentables.at(i).added == FERMENTABLE_ADDED_BOIL) { b += product->fermentables.at(i).amount * (product->fermentables.at(i).yield / 100) * (1 - product->fermentables.at(i).moisture / 100); } } double tot = Utils::sg_to_plato(product->brew_aboil_sg) * (product->brew_aboil_volume / 1.04) * product->brew_aboil_sg * 10 / 1000; tot -= b; // total sugars in wort minus added sugars. if (m > 0) result = round((tot / m * 100) * 10.0) / 10.0; product->brew_aboil_efficiency = result; ui->brew_aboileffShow->setValue(result); calcFermentables(); // This will also recalculate all volumes. } void EditProduct::brew_aboilsg_changed(double val) { if (product->brew_aboil_sg == 0) { product->brew_aboil_sg = product->est_og3; const QSignalBlocker blocker1(ui->brew_aboilsgEdit); ui->brew_aboilsgEdit->setValue(product->est_og3); } else { product->brew_aboil_sg = val; } is_changed(); calcEfficiencyAfterBoil(); } void EditProduct::brew_aboilvol_changed(double val) { if (product->brew_aboil_volume == 0) { product->brew_aboil_volume = product->batch_size * 1.04; const QSignalBlocker blocker1(ui->brew_aboilvolEdit); ui->brew_aboilvolEdit->setValue(product->batch_size * 1.04); } else { product->brew_aboil_volume = val; } is_changed(); calcEfficiencyAfterBoil(); } void EditProduct::brew_aboil_cm_changed(double val) { double vol = Utils::kettle_vol(val, product->eq_kettle_volume, product->eq_kettle_height); product->brew_aboil_volume = vol; const QSignalBlocker blocker1(ui->brew_aboilvolEdit); ui->brew_aboilvolEdit->setValue(vol); is_changed(); calcEfficiencyAfterBoil(); } void EditProduct::brew_aboil_button() { brew_volume_calc(product->brew_aboil_volume, product->eq_kettle_volume, product->eq_kettle_height, product->batch_size * 1.04, true); } void EditProduct::brew_cooling_to_changed(double val) { product->brew_cooling_to = val; is_changed(); } void EditProduct::brew_cooling_time_changed(double val) { product->brew_cooling_time = val; is_changed(); } void EditProduct::brew_whirlpool9_changed(double val) { product->brew_whirlpool9 = val; is_changed(); } void EditProduct::brew_whirlpool7_changed(double val) { product->brew_whirlpool7 = val; is_changed(); } void EditProduct::brew_whirlpool6_changed(double val) { product->brew_whirlpool6 = val; is_changed(); } void EditProduct::brew_whirlpool2_changed(double val) { product->brew_whirlpool2 = val; is_changed(); } void EditProduct::brew_aerwith_changed(int val) { product->brew_aeration_type = val; is_changed(); } void EditProduct::brew_aerspeed_changed(double val) { product->brew_aeration_speed = val; is_changed(); } void EditProduct::brew_aertime_changed(double val) { product->brew_aeration_time = val; is_changed(); } void EditProduct::brew_trubloss_changed(double val) { product->brew_fermenter_tcloss = val; is_changed(); calcFermentables(); // This will also recalculate all volumes. calcIBUs(); } void EditProduct::brew_topupwater_changed(double val) { product->brew_fermenter_extrawater = val; is_changed(); calcFermentables(); // This will also recalculate all volumes. calcIBUs(); } void EditProduct::brew_log_button() { QSqlQuery query; double timestamp; QDialog* dialog = new QDialog(this); dialog->resize(1024, 600); QPushButton *saveButton = new QPushButton(tr("Save")); saveButton->setAutoDefault(false); QIcon icon1; icon1.addFile(QString::fromUtf8(":icons/silk/disk.png"), QSize(), QIcon::Normal, QIcon::Off); saveButton->setIcon(icon1); QDialogButtonBox *buttonBox = new QDialogButtonBox(dialog); buttonBox->setObjectName(QString::fromUtf8("buttonBox")); buttonBox->setOrientation(Qt::Vertical); buttonBox->setStandardButtons(QDialogButtonBox::Ok); buttonBox->addButton(saveButton,QDialogButtonBox::ActionRole); QLineSeries *pv_mlt = new QLineSeries(); pv_mlt->setName("MLT"); QLineSeries *sp_mlt = new QLineSeries(); sp_mlt->setName("Set"); sp_mlt->setOpacity(0.75); QLineSeries *pv_hlt = new QLineSeries(); pv_hlt->setName("HLT"); QLineSeries *pwm_mlt1 = new QLineSeries(); // Top side of area QLineSeries *pwm_mlt0 = new QLineSeries(); // Bottom side of area query.prepare("SELECT * FROM log_brews WHERE code=:code ORDER BY datetime"); query.bindValue(":code", product->code); query.exec(); while (query.next()) { timestamp = query.value("datetime").toDateTime().toSecsSinceEpoch() * 1000; pv_mlt->append(timestamp, query.value("pv_mlt").toDouble()); sp_mlt->append(timestamp, query.value("sp_mlt").toDouble()); if (query.value("pv_hlt").toDouble() > 0) pv_hlt->append(timestamp, query.value("pv_hlt").toDouble()); pwm_mlt0->append(timestamp, 0); pwm_mlt1->append(timestamp, query.value("pwm_mlt").toInt()); } QPen pen(QColorConstants::Svg::navy); pen.setWidth(2); pv_mlt->setPen(pen); QAreaSeries *pwm_mlt = new QAreaSeries(pwm_mlt0, pwm_mlt1); pwm_mlt->setName("MLT pwr"); pwm_mlt->setOpacity(0.25); chart = new QChart(); chart->setTitle(QString("%1 \"%2\"").arg(product->code).arg(product->name)); chart->addSeries(pwm_mlt); // Order is important, first drawn is lowest layer. chart->addSeries(sp_mlt); chart->addSeries(pv_hlt); chart->addSeries(pv_mlt); // Top layer. QDateTimeAxis *axisX = new QDateTimeAxis; axisX->setTickCount(20); axisX->setFormat("HH:mm"); axisX->setTitleText(tr("Time")); axisX->setLabelsFont(QFont("Helvetica", 8, QFont::Normal)); chart->addAxis(axisX, Qt::AlignBottom); pv_mlt->attachAxis(axisX); pv_hlt->attachAxis(axisX); QValueAxis *axisY = new QValueAxis; axisY->setRange(0, 110); axisY->setTickCount(12); axisY->setMinorTickCount(1); axisY->setLabelFormat("%i"); axisY->setTitleText(tr("Temperature °C or Power %")); axisY->setLabelsFont(QFont("Helvetica", 8, QFont::Normal)); chart->addAxis(axisY, Qt::AlignLeft); pwm_mlt->attachAxis(axisY); pv_mlt->attachAxis(axisY); sp_mlt->attachAxis(axisY); pv_hlt->attachAxis(axisY); connect(pv_mlt, &QLineSeries::hovered, this, &EditProduct::tooltip); connect(pv_hlt, &QLineSeries::hovered, this, &EditProduct::tooltip); chartView = new QChartView(chart); chartView->setRenderHint(QPainter::Antialiasing); dialog->setLayout(new QHBoxLayout); dialog->layout()->addWidget(chartView); dialog->layout()->addWidget(buttonBox); connect(buttonBox, SIGNAL(accepted()), dialog, SLOT(accept())); connect(saveButton, SIGNAL(clicked()), this, SLOT(savePNG())); dialog->setModal(true); dialog->exec(); } void EditProduct::savePNG() { QString path = QFileDialog::getSaveFileName(this, tr("Save Image"), QDir::homePath() + "/brewday.png", tr("Image (*.png)")); if (path.isEmpty()) { QMessageBox::warning(this, tr("Save File"), tr("No image file selected.")); return; } QImage img((chartView->size()), QImage::Format_ARGB32); QPainter painter; painter.begin(&img); chartView->render(&painter); painter.setRenderHint(QPainter::Antialiasing); painter.end(); img.save(path); } void EditProduct::tooltip(QPointF point, bool state) { QAbstractSeries *series = qobject_cast<QAbstractSeries *>(sender()); if (t_tooltip == 0) t_tooltip = new Callout(chart, series); if (state) { QDateTime timeis = QDateTime::fromMSecsSinceEpoch(point.x()); t_tooltip->setSeries(series); t_tooltip->setText(QString("%1\n%2 %3°C").arg(timeis.toString("dd-MM-yyyy hh:mm")).arg(series->name()).arg(point.y(), 2, 'f', 1)); t_tooltip->setAnchor(point); t_tooltip->setZValue(11); t_tooltip->updateGeometry(); t_tooltip->show(); } else { t_tooltip->hide(); } }