-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.py
More file actions
1903 lines (1558 loc) · 87 KB
/
Copy pathapp.py
File metadata and controls
1903 lines (1558 loc) · 87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Kombucha Batch Logger & CO₂ Tracker
This Streamlit application helps kombucha brewers track their fermentation batches,
log critical data (pH, temperature, time), and predict CO₂ buildup to avoid
overcarbonation or bottle explosions.
Main features:
1. Batch tracking and management
2. Primary fermentation data logging
3. Secondary fermentation monitoring with CO₂ estimation
4. Batch comparison and visualization
5. Safety alerts for dangerous CO₂ pressure levels
Author: Deen
Email: deen.htc@gmail.com
"""
import streamlit as st
import pandas as pd
import datetime
import plotly.express as px
import json
import os
from co2_calculator import calculate_co2_production, estimate_fermentation_completion, estimate_co2
# File path for persistent storage
DATA_FILE = "kombucha_data.json"
print(f"Data file path: {os.path.abspath(DATA_FILE)}")
# Function to save data to file - define this BEFORE using it
def save_data():
try:
data = {
'batches': st.session_state.batches,
'settings': st.session_state.settings
}
with open(DATA_FILE, 'w') as f:
json.dump(data, f, indent=2)
print(f"Saved {len(st.session_state.batches)} batches to {DATA_FILE}")
except Exception as e:
st.error(f"Error saving data: {str(e)}")
print(f"Error saving data: {str(e)}")
# Set page configuration
st.set_page_config(
page_title="Kombucha Batch Logger",
page_icon="🍵",
layout="wide"
)
# Initialize session state for batch data and settings
if 'batches' not in st.session_state:
st.session_state.batches = []
# Initialize settings if they don't exist
if 'settings' not in st.session_state:
st.session_state.settings = {
'danger_threshold': 2.5,
'warning_threshold': 1.5,
'show_alerts': True,
'alert_check_frequency': 'always' # Options: 'always', 'daily', 'never'
}
# Initialize confirmation flags
if 'confirm_delete' not in st.session_state:
st.session_state.confirm_delete = False
# Load data from file if it exists (do this AFTER initializing session state)
if os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, 'r') as f:
data = json.load(f)
st.session_state.batches = data.get('batches', [])
# Update settings if they exist in the file
if 'settings' in data:
st.session_state.settings.update(data['settings'])
print(f"Loaded {len(st.session_state.batches)} batches from {DATA_FILE}")
except Exception as e:
st.error(f"Error loading data: {e}")
# Sidebar for settings
with st.sidebar:
st.title("Settings")
# Carbonation Alert Settings
st.header("Carbonation Alert Settings")
# Enable/disable alerts
show_alerts = st.checkbox("Show Over-Carbonation Alerts",
value=st.session_state.settings['show_alerts'],
help="Enable or disable alerts for batches at risk of over-carbonation")
# Danger threshold slider
danger_threshold = st.slider(
"Danger Threshold (atm)",
min_value=1.0,
max_value=5.0,
value=float(st.session_state.settings['danger_threshold']),
step=0.1,
help="CO₂ pressure level that triggers a danger alert (risk of bottle explosion)"
)
# Warning threshold slider
warning_threshold = st.slider(
"Warning Threshold (atm)",
min_value=0.5,
max_value=danger_threshold - 0.1,
value=min(float(st.session_state.settings['warning_threshold']), danger_threshold - 0.1),
step=0.1,
help="CO₂ pressure level that triggers a warning alert"
)
# Alert check frequency
alert_check_frequency = st.radio(
"Alert Check Frequency",
options=["Always", "Daily", "Never"],
index=["always", "daily", "never"].index(st.session_state.settings['alert_check_frequency']),
help="How often to check for over-carbonation alerts"
)
# Save settings button
if st.button("Save Settings"):
st.session_state.settings['danger_threshold'] = danger_threshold
st.session_state.settings['warning_threshold'] = warning_threshold
st.session_state.settings['show_alerts'] = show_alerts
st.session_state.settings['alert_check_frequency'] = alert_check_frequency.lower()
save_data() # Save data to file
st.success("Settings saved successfully!")
st.markdown("---")
# App title and description
st.title("Kombucha Batch Logger")
st.markdown("""
This application helps you track your kombucha brewing batches.
Log your batch details and view your brewing history.
⚠️ **SAFETY DISCLAIMER**: The CO₂ pressure estimates provided by this application are based on simplified models and should be used as general guidance only, not as a definitive safety measure. Always follow proper brewing safety practices:
- Use pressure-rated bottles designed for fermentation
- Store bottles in a safe location, preferably in a container that can contain potential breakage
- Never ignore signs of excessive pressure (bulging caps, hissing sounds)
- When in doubt, refrigerate your brew to slow fermentation
The developers of this application are not responsible for any damage, injury, or loss resulting from reliance on these estimates.
""")
# Check if alerts are enabled
if st.session_state.settings['show_alerts']:
# Determine if we should check for alerts based on frequency setting
should_check_alerts = False
if st.session_state.settings['alert_check_frequency'] == 'always':
should_check_alerts = True
elif st.session_state.settings['alert_check_frequency'] == 'daily':
# Check if we've already checked today
today = datetime.datetime.now().strftime('%Y-%m-%d')
if 'last_alert_check' not in st.session_state.settings or st.session_state.settings['last_alert_check'] != today:
should_check_alerts = True
st.session_state.settings['last_alert_check'] = today
if should_check_alerts and st.session_state.batches:
# Get the danger and warning thresholds from settings
danger_threshold = st.session_state.settings['danger_threshold']
warning_threshold = st.session_state.settings['warning_threshold']
# Check all batches for over-carbonation risk
at_risk_batches = []
for batch in st.session_state.batches:
# Skip if the batch doesn't have measurements
if "measurements" not in batch or not batch["measurements"]:
continue
# Get the latest measurement
latest_measurement = sorted(batch["measurements"], key=lambda m: m["date"], reverse=True)[0]
# Check if CO₂ pressure exceeds thresholds
if "co2_pressure" in latest_measurement:
pressure = latest_measurement["co2_pressure"]
risk_level = None
if pressure >= danger_threshold:
risk_level = "danger"
elif pressure >= warning_threshold:
risk_level = "warning"
if risk_level:
at_risk_batches.append({
"name": batch["name"],
"pressure": pressure,
"risk_level": risk_level,
"date": latest_measurement["date"]
})
# Display alerts if any batches are at risk
if at_risk_batches:
st.markdown("### ⚠️ Carbonation Alerts")
# Create columns for the alerts
alert_cols = st.columns([1, 1, 1, 1])
alert_cols[0].markdown("**Batch Name**")
alert_cols[1].markdown("**CO₂ Pressure**")
alert_cols[2].markdown("**Risk Level**")
alert_cols[3].markdown("**Last Measured**")
# Sort batches by risk level (danger first) and then by pressure
at_risk_batches.sort(key=lambda b: (0 if b["risk_level"] == "danger" else 1, -b["pressure"]))
for batch in at_risk_batches:
cols = st.columns([1, 1, 1, 1])
cols[0].write(batch["name"])
cols[1].write(f"{batch['pressure']:.2f} atm")
if batch["risk_level"] == "danger":
cols[2].error("DANGER")
else:
cols[2].warning("Warning")
cols[3].write(batch["date"])
# Replace the View Fermentation Data button with clearer guidance
st.info("""
**To view detailed fermentation data:**
1. Click on the "Primary Fermentation" tab above for batches in initial fermentation
2. Click on the "Secondary Fermentation" tab for bottled batches
This will allow you to view and update measurements for your at-risk batches.
""")
st.markdown("---")
# Initialize the active tab in session state if it doesn't exist
if 'active_tab' not in st.session_state:
st.session_state.active_tab = "batch" # Default to batch management
# Create radio buttons for tab selection instead of tabs
tab_options = ["Batch Management", "Primary Fermentation", "Secondary Fermentation", "Batch Comparison"]
# Map session state values to tab indices
tab_mapping = {
"batch": 0,
"primary": 1,
"secondary": 2,
"comparison": 3
}
# Set the default index based on session state
default_index = tab_mapping.get(st.session_state.active_tab, 0)
# Define a callback function to update session state when radio button changes
def on_tab_change():
# This function will be called when the radio button value changes
# The new value is already in st.session_state.tab_selector
if st.session_state.tab_selector == "Batch Management":
st.session_state.active_tab = "batch"
elif st.session_state.tab_selector == "Primary Fermentation":
st.session_state.active_tab = "primary"
elif st.session_state.tab_selector == "Secondary Fermentation":
st.session_state.active_tab = "secondary"
elif st.session_state.tab_selector == "Batch Comparison":
st.session_state.active_tab = "comparison"
# Create the tab selector with on_change callback
selected_tab = st.radio(
"Select Tab:",
tab_options,
index=default_index,
horizontal=True,
key="tab_selector",
on_change=on_tab_change
)
# Display content based on the active_tab in session state
if st.session_state.active_tab == "batch":
st.header("Batch Management")
st.markdown("""
### 📝 Create and manage your kombucha batches
**Fermentation Phases Explained:**
- **Primary Fermentation**: The initial open-air fermentation with SCOBY in a jar/vessel covered with breathable cloth
- **Secondary Fermentation**: Bottling with optional flavoring in sealed containers to build carbonation
""")
# Create two columns for the main layout
col1, col2 = st.columns([1, 1])
# Batch input form
with col1:
st.header("Log a New Batch")
with st.form("batch_form"):
batch_name = st.text_input("Batch Name", "My Kombucha Batch")
tea_type = st.selectbox(
"Tea Type",
["Black", "Green", "Oolong", "White", "Herbal", "Mixed"]
)
sugar_content = st.number_input(
"Sugar Content (grams)",
min_value=1,
max_value=1000,
value=200
)
start_date = st.date_input(
"Start Date",
datetime.datetime.now()
)
# Add SCOBY source field
scoby_source = st.text_input(
"SCOBY Source",
placeholder="e.g., Home-grown, Friend, Commercial",
help="Where did you get your SCOBY from?"
)
# Add flavoring field
flavoring = st.text_input(
"Flavoring (if any)",
placeholder="e.g., Ginger, Fruit, Herbs",
help="Any flavoring ingredients added to this batch"
)
# Optional additional fields
st.markdown("### Optional Details")
volume = st.number_input(
"Batch Volume (liters)",
min_value=0.1,
max_value=50.0,
value=2.0,
step=0.1
)
notes = st.text_area("Notes", "")
# Submit button
submitted = st.form_submit_button("Log Batch")
if submitted:
# Check if a batch with the same name already exists
existing_batch_names = [batch["name"].lower() for batch in st.session_state.batches]
if batch_name.lower() in existing_batch_names:
st.error(f"A batch with the name '{batch_name}' already exists. Please use a different name.")
else:
# Create a new batch entry
new_batch = {
"name": batch_name,
"tea_type": tea_type,
"sugar_content": sugar_content,
"start_date": start_date.strftime("%Y-%m-%d"),
"scoby_source": scoby_source,
"flavoring": flavoring,
"volume": volume,
"notes": notes,
"logged_at": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"fermentation_phase": "primary" # Default to primary fermentation
}
# Add to session state
st.session_state.batches.append(new_batch)
save_data() # Save data to file
st.success("Batch logged successfully!")
# Display logged batches in the first tab
with col2:
st.header("Logged Batches")
if not st.session_state.batches:
st.info("No batches logged yet. Use the form to log your first batch!")
else:
# Add batch management options
st.subheader("Batch Management")
# Create a selection for batch to delete
batch_names = [batch["name"] for batch in st.session_state.batches]
batch_to_delete = st.selectbox(
"Select a batch to delete",
options=batch_names,
key="batch_delete_selectbox"
)
# Add delete button with confirmation
delete_col1, delete_col2 = st.columns([1, 3])
with delete_col1:
if st.button("Delete Batch", key="delete_batch_button", type="primary"):
st.session_state.confirm_delete = True
with delete_col2:
if st.session_state.get("confirm_delete", False):
st.warning(f"Are you sure you want to delete '{batch_to_delete}'? This cannot be undone.")
confirm_col1, confirm_col2 = st.columns([1, 1])
with confirm_col1:
if st.button("Yes, Delete", key="confirm_delete_yes"):
# Find and remove the selected batch
st.session_state.batches = [
batch for batch in st.session_state.batches
if batch["name"] != batch_to_delete
]
save_data() # Save data to file
st.success(f"Batch '{batch_to_delete}' deleted successfully!")
st.session_state.confirm_delete = False
st.rerun()
with confirm_col2:
if st.button("Cancel", key="confirm_delete_cancel"):
st.session_state.confirm_delete = False
st.rerun()
st.markdown("---")
# Convert the batches to a DataFrame for display
batches_df = pd.DataFrame(st.session_state.batches)
# Handle the measurements column - either exclude it or format it
if "measurements" in batches_df.columns:
# Option 1: Drop the measurements column
batches_df = batches_df.drop(columns=["measurements"])
# Option 2 (alternative): Format the measurements column to show count
# batches_df["measurements"] = batches_df["measurements"].apply(
# lambda m: f"{len(m)} readings" if isinstance(m, list) else "No readings"
# )
# Display the table with all batch information
st.dataframe(
batches_df,
column_config={
"name": "Batch Name",
"tea_type": "Tea Type",
"sugar_content": st.column_config.NumberColumn(
"Sugar (g)",
format="%d g"
),
"start_date": "Start Date",
"scoby_source": "SCOBY Source",
"flavoring": "Flavoring",
"volume": st.column_config.NumberColumn(
"Volume",
format="%.1f L"
),
"notes": "Notes",
"logged_at": "Logged At"
},
hide_index=True,
use_container_width=True
)
# Summary statistics
st.subheader("Batch Statistics")
total_batches = len(st.session_state.batches)
total_sugar = sum(batch["sugar_content"] for batch in st.session_state.batches)
total_volume = sum(batch["volume"] for batch in st.session_state.batches)
st.markdown(f"""
- **Total Batches**: {total_batches}
- **Total Sugar Used**: {total_sugar} grams
- **Total Volume**: {total_volume:.1f} liters
""")
# Tea type distribution
tea_counts = batches_df["tea_type"].value_counts()
st.subheader("Tea Type Distribution")
st.bar_chart(tea_counts)
# Add export to CSV functionality
st.subheader("Export Data")
if st.button("Export All Batch Data to CSV", key="export_all_data"):
# Create a comprehensive dataframe with all batch data
export_data = []
for batch in st.session_state.batches:
# Basic batch info
batch_info = {
"batch_name": batch["name"],
"tea_type": batch["tea_type"],
"sugar_content": batch["sugar_content"],
"start_date": batch["start_date"],
"volume": batch["volume"],
"notes": batch["notes"]
}
# If there are measurements, add each as a row
if "measurements" in batch and batch["measurements"]:
for measurement in batch["measurements"]:
# Combine batch info with measurement data
measurement_row = batch_info.copy()
measurement_row.update({
"measurement_date": measurement["date"],
"temperature": measurement["temperature"],
"ph": measurement["ph"],
"co2_estimate": measurement["co2_estimate"],
"co2_pressure": measurement.get("co2_pressure", "N/A"),
"completion": measurement.get("completion", "N/A")
})
export_data.append(measurement_row)
else:
# If no measurements, just add the batch info
batch_info.update({
"measurement_date": "N/A",
"temperature": "N/A",
"ph": "N/A",
"co2_estimate": "N/A",
"co2_pressure": "N/A",
"completion": "N/A"
})
export_data.append(batch_info)
# Create dataframe from the collected data
export_df = pd.DataFrame(export_data)
# Convert dataframe to CSV
csv = export_df.to_csv(index=False)
# Create a download button
st.download_button(
label="Download CSV File",
data=csv,
file_name="kombucha_batch_data.csv",
mime="text/csv",
help="Click to download all batch data as a CSV file"
)
elif st.session_state.active_tab == "primary":
st.header("Primary Fermentation Tracking")
st.markdown("""
### 🍵 Track your primary fermentation progress
Primary fermentation is the first stage where your SCOBY converts sugar to acids and produces flavor compounds.
This phase typically occurs in an open container covered with a breathable cloth and lasts 7-14 days.
**Main Objective**: Monitor the fermentation completion to determine the optimal time to move to secondary fermentation.
""")
# Create columns with better proportions for the fermentation data section
fcol1, fcol2 = st.columns([1, 1.2])
with fcol1:
# Add a container with border styling
with st.container():
st.subheader("Input Fermentation Data")
# Select a batch if any exist
if not st.session_state.batches:
st.warning("No batches available. Please log a batch first.")
selected_batch = None
else:
# Filter for primary fermentation batches
primary_batches = [b for b in st.session_state.batches
if b.get("fermentation_phase", "primary") == "primary"]
if not primary_batches:
st.warning("No batches in primary fermentation phase. Please create a batch first.")
selected_batch = None
else:
# Add a more visually appealing batch selector
st.markdown("##### Select Your Batch")
batch_options = [f"{b['name']} (started {b['start_date']})" for b in primary_batches]
selected_batch_idx = st.selectbox(
"Select Batch",
range(len(batch_options)),
format_func=lambda i: batch_options[i]
)
selected_batch = primary_batches[selected_batch_idx]
# Calculate days fermenting
start_date = datetime.datetime.strptime(selected_batch['start_date'], "%Y-%m-%d")
today = datetime.datetime.now()
days_fermenting = (today - start_date).days
# Display batch info with last reading date
last_reading_date = "No readings yet"
if "measurements" in selected_batch and selected_batch["measurements"]:
# Filter for primary phase measurements
primary_measurements = [m for m in selected_batch["measurements"] if m.get("phase") == "primary"]
if primary_measurements:
# Get the most recent reading
last_reading = sorted(primary_measurements, key=lambda x: x["date"], reverse=True)[0]
last_reading_date = last_reading["date"]
days_since_reading = (today - datetime.datetime.strptime(last_reading_date, "%Y-%m-%d")).days
if days_since_reading == 0:
last_reading_status = "✅ Today"
elif days_since_reading == 1:
last_reading_status = "⚠️ Yesterday"
else:
last_reading_status = f"❗ {days_since_reading} days ago"
else:
last_reading_status = "❓ No primary readings"
else:
last_reading_status = "❓ No readings"
st.markdown(f"""
<div style="background-color: #1E1E1E; padding: 15px; border-radius: 5px; margin-top: 10px;">
<h5 style="color: #FFFFFF;">Batch Information</h5>
<p style="color: #E5E7EB;"><strong>Tea Type:</strong> {selected_batch['tea_type']}</p>
<p style="color: #E5E7EB;"><strong>Sugar Content:</strong> {selected_batch['sugar_content']}g</p>
<p style="color: #E5E7EB;"><strong>Volume:</strong> {selected_batch['volume']}L</p>
<p style="color: #E5E7EB;"><strong>Days Fermenting:</strong> {days_fermenting} days</p>
<p style="color: #E5E7EB;"><strong>Last Reading:</strong> {last_reading_status}</p>
</div>
""", unsafe_allow_html=True)
# Primary fermentation specific inputs
st.markdown("---")
st.subheader("Current Readings")
# Use columns for the sliders to make them more compact
temp_col, ph_col = st.columns(2)
with temp_col:
temperature = st.slider(
"Temperature (°C)",
min_value=15.0,
max_value=35.0,
value=25.0,
step=0.5,
help="The current temperature of your kombucha batch"
)
with ph_col:
ph_level = st.slider(
"pH Level",
min_value=2.0,
max_value=7.0,
value=3.5,
step=0.1,
help="The current pH level of your kombucha batch"
)
# Primary fermentation specific metrics
taste = st.select_slider(
"Taste Profile",
options=["Very Sweet", "Sweet", "Balanced", "Tart", "Sour", "Very Sour"],
value="Balanced",
help="How does your kombucha taste currently?"
)
# Add Brix measurement
brix = st.number_input(
"Brix (°Bx)",
min_value=0.0,
max_value=20.0,
value=6.0,
step=0.1,
help="Sugar content measured with a refractometer or hydrometer (in degrees Brix)"
)
# Remove SCOBY thickness input field
# scoby_thickness = st.number_input(
# "SCOBY Thickness (mm)",
# min_value=0,
# max_value=50,
# value=5,
# step=1,
# help="Approximate thickness of your SCOBY in millimeters"
# )
# Check if there's already a reading for today
today_str = today.strftime("%Y-%m-%d")
has_reading_today = False
if "measurements" in selected_batch:
for measurement in selected_batch["measurements"]:
if measurement["date"] == today_str and measurement["phase"] == "primary":
has_reading_today = True
break
# Display reading frequency guidance
st.markdown("---")
st.subheader("Reading Frequency")
st.info("""
**Recommended Reading Schedule**:
- Take readings once daily, ideally at the same time each day
- More frequent readings during the first 3-5 days can help track the initial fermentation curve
- Consistent daily readings provide the most accurate fermentation progress tracking
**What These Readings Tell You**:
- **pH**: Decreases as fermentation progresses (starts ~4.5, finishes ~2.8-3.2)
- **Brix**: Measures sugar content, decreases as sugar is consumed (starts ~8-12, finishes ~2-4)
- **Temperature**: Affects fermentation speed (optimal: 23-28°C)
- **Taste**: Subjective assessment that helps correlate with objective measurements
""")
# Save readings button with improved styling
st.markdown("---")
# Show warning if already has reading for today
if has_reading_today:
st.warning("⚠️ You've already recorded a reading for today. Adding another will create a duplicate entry for today's date.")
else:
st.success("✅ No reading recorded for today yet. It's a good time to add your daily measurement!")
if st.button("💾 Record Readings", use_container_width=True, key="save_primary_readings"):
# Initialize measurements list if it doesn't exist
if "measurements" not in selected_batch:
selected_batch["measurements"] = []
# Add new measurement (without SCOBY thickness)
selected_batch["measurements"].append({
"date": today.strftime("%Y-%m-%d"),
"temperature": temperature,
"ph": ph_level,
"taste": taste,
"brix": brix,
"phase": "primary"
})
save_data() # Save data to file
st.success("Readings saved successfully!")
# Add option to move to secondary fermentation
st.markdown("---")
st.subheader("Ready for Bottling?")
if st.button("Move to Secondary Fermentation", use_container_width=True, key="move_to_secondary"):
# Update the batch to secondary phase
selected_batch["fermentation_phase"] = "secondary"
selected_batch["bottling_date"] = today.strftime("%Y-%m-%d")
save_data()
st.success("Batch moved to secondary fermentation! Please go to the Secondary Fermentation tab to add bottling details.")
st.balloons()
with fcol2:
if selected_batch:
# Add a container with styling
with st.container():
st.subheader("CO₂ Production Estimate")
# Calculate CO2 production and completion percentage
co2_produced = calculate_co2_production(
sugar_amount=selected_batch['sugar_content'],
days=days_fermenting,
temperature=temperature,
volume=selected_batch['volume']
)
completion_pct = estimate_fermentation_completion(
sugar_amount=selected_batch['sugar_content'],
co2_produced=co2_produced
)
# Create a two-column layout for the metrics
metric_col1, metric_col2 = st.columns(2)
with metric_col1:
# Display CO₂ production estimate with icon
st.markdown("##### 🧪 CO₂ Produced")
st.metric(
"Amount",
f"{co2_produced:.2f} g",
delta=f"{completion_pct:.1f}% of potential"
)
with metric_col2:
# Calculate CO₂ pressure
co2_pressure = estimate_co2(
sugar_content=selected_batch['sugar_content'],
temp=temperature,
time_in_days=days_fermenting
)
# Get thresholds from settings
danger_threshold = st.session_state.settings['danger_threshold']
warning_threshold = st.session_state.settings['warning_threshold']
# Display CO₂ pressure estimate with warning levels and icon
st.markdown("##### 📊 CO₂ Pressure")
pressure_color = "normal"
if co2_pressure >= danger_threshold:
pressure_color = "off"
pressure_warning = f"⚠️ Danger! (>{danger_threshold} atm)"
elif co2_pressure >= warning_threshold:
pressure_color = "inverse"
pressure_warning = f"⚠️ High (>{warning_threshold} atm)"
else:
pressure_warning = f"✅ Safe (<{warning_threshold} atm)"
st.metric(
"Pressure",
f"{co2_pressure:.2f} atm",
delta=pressure_warning,
delta_color=pressure_color
)
# Create a progress bar for fermentation completion with better styling
st.markdown("---")
st.markdown("### Fermentation Progress")
# Add percentage text above progress bar
st.markdown(f"<h4 style='text-align: center; color: {'green' if completion_pct < 70 else 'orange' if completion_pct < 90 else 'red'};'>{completion_pct:.1f}%</h4>", unsafe_allow_html=True)
# Progress bar
progress_color = "green" if completion_pct < 70 else "orange" if completion_pct < 90 else "red"
st.progress(min(completion_pct / 100, 1.0))
# Add interpretation text
if completion_pct < 30:
st.info("🌱 Early fermentation stage - sweet with mild acidity")
elif completion_pct < 70:
st.success("🍵 Mid fermentation stage - balanced sweetness and acidity")
elif completion_pct < 90:
st.warning("🔶 Late fermentation stage - becoming more acidic")
else:
st.error("🔴 Final fermentation stage - highly acidic, minimal sweetness")
# Add Brix interpretation
st.markdown("---")
st.markdown("### Brix Interpretation")
if brix > 8:
st.info("🍯 High sugar content - fermentation is in early stages")
elif brix > 5:
st.success("🍵 Medium sugar content - fermentation is progressing well")
elif brix > 3:
st.warning("🔶 Low sugar content - fermentation is nearing completion")
else:
st.error("🔴 Very low sugar content - fermentation is complete or nearly complete")
st.markdown("""
**Brix Measurement Guide**:
- Starting kombucha tea: ~8-12 °Bx
- Mid-fermentation: ~5-8 °Bx
- Ready to bottle: ~3-5 °Bx
- Fully fermented: <3 °Bx
A steady decrease in Brix readings over time indicates active fermentation.
""")
# Display pressure gauge visualization with improved styling
st.markdown("---")
st.markdown("### CO₂ Pressure Gauge")
# Add disclaimer about pressure estimates
st.info("""
**Note on Accuracy**: This pressure estimate is based on a simplified model that doesn't account for all variables in real fermentation environments. Factors like microbial composition, oxygen levels, and previous fermentation history can all affect actual CO₂ production. Always use physical signs (bottle firmness, cap bulging) alongside these estimates.
""")
# Calculate gauge range and steps based on thresholds
max_gauge_value = max(3.0, danger_threshold * 1.2) # Set max to at least 20% above danger threshold
# Create steps for the gauge
gauge_steps = [
{"range": [0, warning_threshold], "color": "green"},
{"range": [warning_threshold, danger_threshold], "color": "yellow"},
{"range": [danger_threshold, max_gauge_value], "color": "red"},
]
pressure_gauge = {
"data": [
{
"type": "indicator",
"mode": "gauge+number",
"value": co2_pressure,
"title": {"text": "Pressure (atm)"},
"gauge": {
"axis": {"range": [0, max_gauge_value], "tickwidth": 1},
"bar": {"color": "darkblue"},
"bgcolor": "white",
"borderwidth": 2,
"bordercolor": "gray",
"steps": gauge_steps,
"threshold": {
"line": {"color": "red", "width": 4},
"thickness": 0.75,
"value": danger_threshold,
},
},
}
],
"layout": {"height": 250, "margin": {"t": 25, "b": 25, "l": 25, "r": 25}},
}
st.plotly_chart(pressure_gauge, use_container_width=True)
# Display pH level interpretation
st.markdown("### pH Level Interpretation")
if ph_level > 4.5:
st.warning("pH is high. Fermentation may be just starting.")
elif ph_level > 3.5:
st.info("pH is in a good range for early fermentation.")
elif ph_level > 2.8:
st.success("pH is in the ideal range for kombucha.")
else:
st.warning("pH is getting low. Your kombucha may be very sour.")
# Display temperature interpretation
st.markdown("### Temperature Interpretation")
if temperature < 20:
st.warning("Temperature is low. Fermentation will be slower.")
elif temperature < 24:
st.info("Temperature is in a good range, but slightly cool.")
elif temperature <= 29:
st.success("Temperature is in the ideal range for kombucha fermentation.")
else:
st.warning("Temperature is high. Watch for mold or over-fermentation.")
# Show CO₂ production over time if measurements exist
if "measurements" in selected_batch and selected_batch["measurements"]:
st.markdown("---")
st.subheader("📈 Measurement History")
# Create a dataframe from measurements
measurements_df = pd.DataFrame(selected_batch["measurements"])
measurements_df["date"] = pd.to_datetime(measurements_df["date"])
# Create tabs for different charts with custom styling
chart_tab1, chart_tab2, chart_tab3 = st.tabs(["📊 CO₂ Production", "📈 CO₂ Pressure", "🔍 Data Table"])
with chart_tab1:
# Create a line chart of CO₂ production over time with improved styling
fig1 = px.line(
measurements_df,
x="date",
y="co2_estimate",
title="CO₂ Production Over Time",
labels={"date": "Date", "co2_estimate": "CO₂ (g)"},
markers=True
)
# Customize the chart appearance
fig1.update_traces(line=dict(width=3), marker=dict(size=8))
fig1.update_layout(
plot_bgcolor="rgba(240, 242, 246, 0.8)",
paper_bgcolor="rgba(0,0,0,0)",
font=dict(size=12),
height=400
)
st.plotly_chart(fig1, use_container_width=True)
with chart_tab2:
# Create a line chart of CO₂ pressure over time with improved styling
fig2 = px.line(
measurements_df,
x="date",
y="co2_pressure",
title="CO₂ Pressure Over Time",
labels={"date": "Date", "co2_pressure": "Pressure (atm)"},
markers=True
)
# Customize the chart appearance
fig2.update_traces(line=dict(width=3, color="#2E86C1"), marker=dict(size=8))
fig2.update_layout(
plot_bgcolor="rgba(240, 242, 246, 0.8)",
paper_bgcolor="rgba(0,0,0,0)",
font=dict(size=12),
height=400
)
# Get thresholds from settings
danger_threshold = st.session_state.settings['danger_threshold']
warning_threshold = st.session_state.settings['warning_threshold']
# Add danger threshold line
fig2.add_hline(
y=danger_threshold,
line_dash="dash",
line_color="red",
line_width=2,
annotation_text=f"Danger Level ({danger_threshold} atm)",
annotation_font=dict(color="red")
)
# Add warning threshold line
fig2.add_hline(
y=warning_threshold,
line_dash="dot",
line_color="orange",
line_width=2,
annotation_text=f"Warning Level ({warning_threshold} atm)",
annotation_font=dict(color="orange")
)
st.plotly_chart(fig2, use_container_width=True)
with chart_tab3:
# Display the measurements table with improved styling
st.dataframe(
measurements_df,
column_config={
"date": "Date",
"temperature": st.column_config.NumberColumn("Temp (°C)", format="%.1f °C"),
"ph": st.column_config.NumberColumn("pH", format="%.1f"),
"brix": st.column_config.NumberColumn("Brix", format="%.1f °Bx"),
"co2_estimate": st.column_config.NumberColumn("CO₂ (g)", format="%.2f g"),
"co2_pressure": st.column_config.NumberColumn("Pressure (atm)", format="%.2f atm"),
"completion": st.column_config.ProgressColumn("Completion", format="%.1f%%", min_value=0, max_value=100)
},