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
+19 -12
View File
@@ -55,17 +55,20 @@ class SudTask(ATask):
self.forecast_confirm_marks = {}
msg_handler.set_recv_handler(self.recv)
def apply_plant_params(self, step):
def apply_plant_params(self, grain_mass, water_mass):
"""Keeps the real plant's and the controller's internal model's
plant params in sync with this Sud's own doc - L/Td come straight
from it (constant for the whole brew), while M/C also fold in the
step's grain_mass/water_mass, which vary over the brew's course
given grain_mass/water_mass, which vary over the brew's course
(malt going in, water boiling off) - mirrors demo_sud.py's
apply_plant_params(). Called both on every real step transition
(via on_step_changed()) and once immediately on Load (see recv()),
so the controller's behavior already matches the expected plant
as soon as a Sud is loaded, not just once a run actually starts."""
params = self.sud.derive_plant_params(step.get('grain_mass', 0), step.get('water_mass', 0))
(via on_step_changed(), with that step's own grain_mass/water_mass)
and once immediately on Load (see recv(), with the doc's own
initial Sud.grain_mass/water_mass - no need to parse the first
step out of the schedule for that), so the controller's behavior
already matches the expected plant as soon as a Sud is loaded,
not just once a run actually starts."""
params = self.sud.derive_plant_params(grain_mass, water_mass)
self.pot.set_plant_params(params)
if hasattr(self.tc, 'set_model_plant_params'):
self.tc.set_model_plant_params(params)
@@ -95,7 +98,7 @@ class SudTask(ATask):
phase = ramp if ramping else hold
if step is not None:
self.apply_plant_params(step)
self.apply_plant_params(step.get('grain_mass', 0), step.get('water_mass', 0))
if ramping and step['temperature'] is not None:
self.tc.set_theta_soll(step['temperature'])
self.tc.set_heatrate_soll(ramp['rate'])
@@ -214,6 +217,10 @@ class SudTask(ATask):
'Description': self.sud.description,
'pot_mass': self.sud.pot_mass,
'pot_material': self.sud.pot_material,
'L': self.sud.L,
'Td': self.sud.Td,
'grain_mass': self.sud.grain_mass,
'water_mass': self.sud.water_mass,
'steps': schedule[index:],
}
start_theta = self.tc.get_theta_ist()
@@ -264,12 +271,12 @@ class SudTask(ATask):
# on_step_changed() only re-applies plant params once a
# real step starts (Start) - apply them right away too,
# so the controller already matches this Sud's own pot/
# L/Td (and its first step's expected grain/water mass)
# from the moment it's loaded, rather than whatever the
# previously loaded Sud (or the generic startup
# baseline - see server/brewpi.py) left behind.
# L/Td/initial grain_mass/water_mass from the moment
# it's loaded, rather than whatever the previously
# loaded Sud (or the generic startup baseline - see
# server/brewpi.py) left behind.
if self.sud.schedule:
self.apply_plant_params(self.sud.schedule[0])
self.apply_plant_params(self.sud.grain_mass, self.sud.water_mass)
await self.send_forecast(pair[1])
else:
# Sud.load() refuses while a run is in progress (state