From 194156f2442d07ecfbfb9146650ca0fcc64b0cf9 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sun, 21 Jun 2026 09:47:45 +0200 Subject: [PATCH] Move Sud step's target temperature from ramp.temp to step.temperature A step's target temperature applies to the whole step (it's what a ramp ramps to and what a hold then holds at), not just its ramp phase - promote it from a nested "ramp": {"temp": ...} to a step-level "temperature" field, defaulted like descr/grain_mass/etc. Updates all consumers (Sud._build_step, SudTask, the GUI's forecast estimator, demo_sud.py) and migrates sude/sud_0010.json and the README. --- README.md | 14 ++++++++------ client/brewpi_gui.py | 2 +- components/sud.py | 2 +- scripts/demos/sud/demo_sud.py | 6 +++--- sude/sud_0010.json | 24 ++++++++++++------------ tasks/sud.py | 6 +++--- 6 files changed, 28 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index f2aab1f..dd1ff02 100644 --- a/README.md +++ b/README.md @@ -130,17 +130,19 @@ environment (`$WORKON_HOME`/`$BREWPI_HOME`) on a Raspberry Pi-style deployment. Files under `sude/` describe a brew's mash schedule ("Sud"): `pot_mass`, `pot_material` (fixed for the whole brew), and a `steps` list, each a ramp, a -hold, or both: +hold, or both, plus a step-level `temperature` (the target a ramp step ramps +to, and what a hold step holds at): -- a ramp — `"ramp": {"rate": ..., "temp": ...}` — ramp to `temp` at `rate` +- a ramp — `"ramp": {"rate": ...}` — ramp to `temperature` at `rate` (°C/min), and/or - a hold — `"hold": {"duration": ...}` — hold the current target for `duration` minutes (omit/`0` for an immediate step). -A step with both ramps to `temp` and then holds there for `duration` - -useful for a mash rest ("ramp to 63°C, then hold 40 min") without needing two -separate schedule entries. `user_wait_for_continue`/`user_message` (below) -apply once the whole step is done, i.e. after the hold phase if there is one. +A step with both ramps to `temperature` and then holds there for `duration` +- useful for a mash rest ("ramp to 63°C, then hold 40 min") without needing +two separate schedule entries. `user_wait_for_continue`/`user_message` +(below) apply once the whole step is done, i.e. after the hold phase if +there is one. Both `ramp` and `hold` carry a `stirrer` block (`speed`, `interval_time`, `on_ratio`): `interval_time: 0` runs the stirrer continuously at `speed`; diff --git a/client/brewpi_gui.py b/client/brewpi_gui.py index 160897b..e4ffa0a 100755 --- a/client/brewpi_gui.py +++ b/client/brewpi_gui.py @@ -139,7 +139,7 @@ class SudForecastPlot(FigureCanvasQTAgg): hold = step.get('hold') if ramp is not None: rate = ramp.get('rate', 0) - target = ramp['temp'] + target = step['temperature'] if rate > 0: t.append(t[-1] + abs(target - theta[-1]) / rate * 60.0) theta.append(target) diff --git a/components/sud.py b/components/sud.py index f5fa6de..1136499 100644 --- a/components/sud.py +++ b/components/sud.py @@ -38,7 +38,7 @@ def _build_step(number, defaults, raw_step): ramp or a hold depending on which of those keys it specifies; the other is left out rather than synthesized from defaults.""" step = {'number': number} - for key in ('descr', 'user_message', 'user_wait_for_continue', 'grain_mass', 'water_mass'): + for key in ('descr', 'user_message', 'user_wait_for_continue', 'grain_mass', 'water_mass', 'temperature'): step[key] = raw_step.get(key, defaults.get(key)) for key in ('ramp', 'hold'): if key in raw_step: diff --git a/scripts/demos/sud/demo_sud.py b/scripts/demos/sud/demo_sud.py index 7be4d66..1d2f061 100644 --- a/scripts/demos/sud/demo_sud.py +++ b/scripts/demos/sud/demo_sud.py @@ -81,15 +81,15 @@ if __name__ == '__main__': # A ramp step whose target is below the current temperature can # only be reached by passive cooling - force the heater off, # mirroring tasks/sud.py's SudTask. - cooling = ramping and 'ramp' in step and step['ramp']['temp'] < ctrl.get_theta_ist() - TEMP_REACHED_TOLERANCE + cooling = bool(ramping and 'ramp' in step and step['temperature'] < ctrl.get_theta_ist() - TEMP_REACHED_TOLERANCE) ctrl.set_cooling(cooling) if cooling: - print(" -> cooling down to {}".format(step['ramp']['temp'])) + print(" -> cooling down to {}".format(step['temperature'])) print(f"Step {step['number']}: {step['descr']}") apply_plant_params(step) if ramping and 'ramp' in step: - ctrl.set_theta_soll(step['ramp']['temp']) + ctrl.set_theta_soll(step['temperature']) ctrl.set_heatrate_soll(step['ramp']['rate']) apply_stirrer(phase) diff --git a/sude/sud_0010.json b/sude/sud_0010.json index 9c66d71..31c18b9 100644 --- a/sude/sud_0010.json +++ b/sude/sud_0010.json @@ -10,9 +10,9 @@ "user_wait_for_continue": false, "grain_mass": 5.21, "water_mass": 22, + "temperature": 0, "ramp": { "rate": 1.0, - "temp": 0, "stirrer": { "speed": 50, "interval_time": 0, @@ -32,18 +32,18 @@ "steps": [ { "grain_mass": 0, - "descr": "Heizen f\u00fcr Einmaischen", + "descr": "Heizen für Einmaischen", + "temperature": 57, "ramp": { "rate": 1.8, - "temp": 57, "stirrer": { "speed": 75 } } }, { - "descr": "Einmaischen einf\u00fcllen", - "user_message": "Bitte Malz einf\u00fcllen und best\u00e4tigen", + "descr": "Einmaischen einfüllen", + "user_message": "Bitte Malz einfüllen und bestätigen", "user_wait_for_continue": true, "hold": { "stirrer": { @@ -59,9 +59,9 @@ }, { "descr": "Glucose-Rast", + "temperature": 63, "ramp": { - "rate": 1.0, - "temp": 63 + "rate": 1.0 }, "hold": { "duration": 40, @@ -74,9 +74,9 @@ }, { "descr": "Verzuckerungs-Rast", + "temperature": 72, "ramp": { - "rate": 1.0, - "temp": 72 + "rate": 1.0 }, "hold": { "duration": 30, @@ -91,9 +91,9 @@ "descr": "Abmaischen", "user_message": "Quittieren zum Abmaischen", "user_wait_for_continue": true, + "temperature": 76, "ramp": { - "rate": 1.0, - "temp": 76 + "rate": 1.0 }, "hold": { "duration": 30, @@ -103,4 +103,4 @@ } } ] -} \ No newline at end of file +} diff --git a/tasks/sud.py b/tasks/sud.py index c280228..680c00b 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -69,13 +69,13 @@ class SudTask(ATask): # bool(...): get_theta_ist() can be a numpy.float64 (Pot's transport # delay line is a numpy array internally) - the '<' comparison would # then yield numpy.bool_, which json.dumps() can't serialize. - cooling = bool(ramping and ramp is not None and ramp['temp'] < self.tc.get_theta_ist() - TEMP_REACHED_TOLERANCE) + cooling = bool(ramping and ramp is not None and step['temperature'] < self.tc.get_theta_ist() - TEMP_REACHED_TOLERANCE) self.tc.set_cooling(cooling) if step is not None: self.apply_plant_params(step) if ramping and ramp is not None: - self.tc.set_theta_soll(ramp['temp']) + self.tc.set_theta_soll(step['temperature']) self.tc.set_heatrate_soll(ramp['rate']) self.apply_stirrer(phase) @@ -83,7 +83,7 @@ class SudTask(ATask): 'Index': self.sud.index, 'Type': 'ramp' if ramping and ramp is not None else 'hold' if hold is not None else None, 'Descr': step.get('descr') if step else None, - 'Temp': ramp.get('temp') if ramp else None, + 'Temp': step.get('temperature') if ramp is not None else None, 'Rate': ramp.get('rate') if ramp else None, 'Duration': hold.get('duration') if (hold is not None and not ramping) else None, 'WaitForUser': step.get('user_wait_for_continue', False) if step else None,