Add explicit initial grain_mass/water_mass to the Sud doc

Sud now parses top-level grain_mass/water_mass (sibling to pot_mass/
pot_material/L/Td, defaulting to 0/0) - what's actually in the pot before
the brew starts, as opposed to default.step's grain_mass/water_mass, which
is just inert template filler. All sude/*.json docs gain explicit values
matching what their first step already resolved to.

tasks/sud.py's apply_plant_params() now takes grain_mass/water_mass
directly instead of a step dict: on_step_changed() still passes the active
step's own (these vary as malt goes in/water boils off), but the Load
handler now reads Sud.grain_mass/water_mass directly instead of parsing
schedule[0]. The GUI's status-line mass preview (shown immediately on
Load, before Start) does the same.

Also fixes a latent bug found along the way: _continue_forecast_after_confirm()'s
reconstructed sub-doc was missing L/Td/grain_mass/water_mass entirely,
silently falling back to generic defaults instead of the real Sud's own
values - invisible only because every current sude/*.json happens to use
those same defaults.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk
This commit is contained in:
2026-06-22 20:19:25 +02:00
co-authored by Claude Sonnet 4.6
parent b925934e57
commit 5ccfcef679
8 changed files with 96 additions and 36 deletions
+48 -18
View File
@@ -285,6 +285,8 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.sud_name = ''
self.sud_schedule = []
self.sud_pot_mass = 0
self.sud_grain_mass = 0
self.sud_water_mass = 0
self.sud_step_index = None
self.sud_step_descr = None
self.sud_step_type = None
@@ -488,6 +490,8 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.sud_name = ''
self.sud_schedule = []
self.sud_pot_mass = 0
self.sud_grain_mass = 0
self.sud_water_mass = 0
self.sud_step_index = None
self.sud_step_descr = None
self.sud_step_type = None
@@ -847,37 +851,63 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.forecast_plot.show_forecast(t_min, forecast['Theta'], self.sud_name, forecast['Finished'])
def update_status_step_label(self):
if not self.sud_step_descr:
if self.sud_step_descr:
text = "Step {}: {}".format(self.sud_step_index, self.sud_step_descr)
if self.sud_step_type == 'hold':
remaining = max(self.sud_hold_remaining, 0.0)
text += " ({:.0f}:{:02.0f} remaining)".format(remaining // 60, remaining % 60)
# sud_step_index is the same 0-based index Sud.index uses into
# its own schedule list (see components/sud.py) - self.
# sud_schedule is built from the very same doc, so it lines up
# directly; grain_mass/water_mass are already resolved against
# default.step there (see components/sud.py's _build_step()).
if 0 <= self.sud_step_index < len(self.sud_schedule):
step = self.sud_schedule[self.sud_step_index]
grain, water = step.get('grain_mass', 0), step.get('water_mass', 0)
else:
grain, water = self.sud_grain_mass, self.sud_water_mass
elif self.sud_schedule:
# Loaded but not started yet (no real Step message has
# arrived) - preview the doc's own initial grain_mass/
# water_mass right away instead of leaving the status line
# blank until Start, mirroring tasks/sud.py's SudTask.
# apply_plant_params() applying it to the real plant/
# controller immediately on Load too.
text = ""
grain, water = self.sud_grain_mass, self.sud_water_mass
else:
self.statusBar().clearMessage()
return
text = "Step {}: {}".format(self.sud_step_index, self.sud_step_descr)
if self.sud_step_type == 'hold':
remaining = max(self.sud_hold_remaining, 0.0)
text += " ({:.0f}:{:02.0f} remaining)".format(remaining // 60, remaining % 60)
# sud_step_index is the same 0-based index Sud.index uses into its
# own schedule list (see components/sud.py) - self.sud_schedule is
# built from the very same doc, so it lines up directly; grain_mass/
# water_mass are already resolved against default.step there (see
# components/sud.py's _build_step()).
if self.sud_step_index is not None and 0 <= self.sud_step_index < len(self.sud_schedule):
step = self.sud_schedule[self.sud_step_index]
pot = self.sud_pot_mass
water = step.get('water_mass', 0)
grain = step.get('grain_mass', 0)
text += " Pot: {:.2f} kg, Water: {:.2f} kg, Grain {:.2f} kg, total {:.2f} kg".format(
pot, water, grain, pot + water + grain)
pot = self.sud_pot_mass
mass_text = "Pot: {:.2f} kg, Water: {:.2f} kg, Grain {:.2f} kg, total {:.2f} kg".format(
pot, water, grain, pot + water + grain)
text = text + " " + mass_text if text else mass_text
self.statusBar().showMessage(text)
def update_sud_forecast(self, doc):
try:
name, _, schedule, pot_mass, _, _, _ = Sud._parse_data(doc)
name, _, schedule, pot_mass, _, _, _, grain_mass, water_mass = Sud._parse_data(doc)
except (KeyError, TypeError):
return
self.sud_name = name
self.sud_schedule = schedule
self.sud_pot_mass = pot_mass
self.sud_grain_mass = grain_mass
self.sud_water_mass = water_mass
# A fresh Load means no step has actually started yet, regardless
# of whatever the previously loaded Sud last reported - don't wait
# for the server's own 'Step' (None) reset (asyncio.create_task()
# there means its arrival order relative to this doc isn't
# guaranteed) to clear a stale step from the *previous* schedule
# before showing this one's preview below.
self.sud_step_descr = None
self.sud_step_index = None
self.sud_step_type = None
self.sud_hold_remaining = 0.0
self.sud_empty = len(schedule) == 0
self.update_sud_actions()
self.update_status_step_label()
if self.sud_empty:
self.forecast_plot.show_no_schedule()
return