refactor: move pot hardware params and pid_type out of per-brew docs

Pot hardware constants (mass, material, L, Td) moved from sude/*.json
to config.json's Pot section; Sud/SudForecastEstimator accept pot_config
as baseline, per-brew pot sections may still override. Controller key
removed; pid_type moved into TempCtrl.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 19:41:04 +02:00
co-authored by Claude Sonnet 4.6
parent 0495e177c4
commit 5142fcbf3d
6 changed files with 41 additions and 26 deletions
+20 -10
View File
@@ -82,28 +82,38 @@ EMPTY_SUD = {
class Sud(AttributeChange):
def __init__(self):
def __init__(self, pot_config=None):
"""Starts out with no schedule loaded - one of potentially several
sude/*.json files on disk is brought in later via load(), kept
purely in memory (see load()) until replaced or the server
restarts."""
restarts.
pot_config provides hardware baseline values (mass, material, L, Td)
from config.json's Pot section - a loaded sud.json may override any
of them per-brew, but config is the source of truth for the physical
kettle."""
AttributeChange.__init__(self)
self._pot_config = pot_config or {}
self._data = EMPTY_SUD
(self.name, self.description, self.schedule, self.pot_mass,
self.pot_material, self.L, self.Td, self.grain_mass,
self.water_mass) = self._parse_data(self._data)
self.water_mass) = self._parse_data(self._data, self._pot_config)
self._paused_from = None
self._reset_run_state()
@staticmethod
def _parse_data(data):
def _parse_data(data, pot_config=None):
"""Parses a sud.json document into the (name, description, schedule,
pot_mass, pot_material, L, Td, grain_mass, water_mass) tuple Sud
needs. Computed up front rather than assigned straight onto self,
so a malformed load() can't leave a half-applied schedule in
place."""
place.
pot_config (from config.json's Pot section) provides hardware baseline
values; a sud.json's own pot section overrides any of them per-brew."""
pot_config = pot_config or {}
name = data.get('Name', '')
description = data.get('Description', '')
@@ -111,10 +121,10 @@ class Sud(AttributeChange):
schedule = [_build_step(i + 1, step_defaults, raw) for i, raw in enumerate(data['steps'])]
pot_data = data.get('pot', {})
pot_mass = pot_data.get('mass', 0)
pot_material = pot_data.get('material')
L = pot_data.get('L', EMPTY_SUD['pot']['L'])
Td = pot_data.get('Td', EMPTY_SUD['pot']['Td'])
pot_mass = pot_data.get('mass', pot_config.get('mass', 0))
pot_material = pot_data.get('material', pot_config.get('material'))
L = pot_data.get('L', pot_config.get('L', EMPTY_SUD['pot']['L']))
Td = pot_data.get('Td', pot_config.get('Td', EMPTY_SUD['pot']['Td']))
grain_mass = pot_data.get('grain_mass', EMPTY_SUD['pot']['grain_mass'])
water_mass = pot_data.get('water_mass', EMPTY_SUD['pot']['water_mass'])
@@ -147,7 +157,7 @@ class Sud(AttributeChange):
if self.state not in (SudState.IDLE, SudState.DONE):
return False
try:
parsed = self._parse_data(data)
parsed = self._parse_data(data, self._pot_config)
except (KeyError, TypeError):
return False
+3 -2
View File
@@ -51,7 +51,7 @@ class SudForecastEstimator:
real run actually is (the discrete steps end up duty-cycling it back
down to something close to the commanded average)."""
def __init__(self, dt, theta_amb, pid_type, tempctrl_params, heater_powers, sim_warp_factor):
def __init__(self, dt, theta_amb, pid_type, tempctrl_params, heater_powers, sim_warp_factor, pot_config=None):
self.dt = dt
self.theta_amb = theta_amb
self.pid_type = pid_type
@@ -61,6 +61,7 @@ class SudForecastEstimator:
# pair straddling the commanded power.
self.heater_powers = heater_powers
self.sim_warp_factor = sim_warp_factor
self.pot_config = pot_config or {}
def set_ambient_temperature(self, theta_amb):
self.theta_amb = theta_amb
@@ -93,7 +94,7 @@ class SudForecastEstimator:
if start_theta is None:
start_theta = self.theta_amb
sud = Sud()
sud = Sud(self.pot_config)
if not sud.load(doc) or not sud.schedule:
return [0.0], [start_theta], SudState.DONE, {}
+7 -3
View File
@@ -1,9 +1,7 @@
{
"ambient_temperature": 20,
"Controller" : {
"pid_type": "Smith"
},
"TempCtrl": {
"pid_type": "Smith",
"beta": 0.05,
"Hold": {
"kp": 0.4,
@@ -45,5 +43,11 @@
"TempSensor": {
"type": "max31865",
"temp_offset": -0.15
},
"Pot": {
"mass": 5.96,
"material": "Edelstahl 18/10",
"L": 0.2,
"Td": 17
}
}
+7 -3
View File
@@ -1,9 +1,7 @@
{
"ambient_temperature": 20,
"Controller" : {
"pid_type": "Smith"
},
"TempCtrl": {
"pid_type": "Smith",
"beta": 0.05,
"Hold": {
"kp": 0.4,
@@ -44,5 +42,11 @@
"sigma": 0.05,
"stirrer_sigma": 0.0,
"stirrer_tau": 20.0
},
"Pot": {
"mass": 5.96,
"material": "Edelstahl 18/10",
"L": 0.2,
"Td": 17
}
}
+4 -4
View File
@@ -81,7 +81,7 @@ if __name__ == '__main__':
# Mash schedule - starts out empty; a client loads one of sude/*.json's
# several schedules onto it later (see components/sud.py's Sud).
sud = Sud()
sud = Sud(config.get('Pot', {}))
# Heater/Pot/Sensor - built together as one consistent rig; each
# component's type comes from its own config section. Heater.type==sim
@@ -113,7 +113,7 @@ if __name__ == '__main__':
# temp_controller_smith.py.), and Smith's model plant params are -
# like the real Pot's above - deliberately left unset here, only
# ever coming from a Sud's own doc.
tc = PidFactory.create(config['Controller']['pid_type'], DT)
tc = PidFactory.create(config['TempCtrl']['pid_type'], DT)
tc.set_params(config['TempCtrl'])
tc.set_ambient_temperature(theta_amb)
tc_task = TcTask(tc, DT_TASK, dispatcher.msgio_get("TempCtrl"))
@@ -128,8 +128,8 @@ if __name__ == '__main__':
# it with the same kind of plant/controller (and params) as above -
# see components/sud_forecast.py.
forecast_estimator = SudForecastEstimator(
DT, theta_amb, config['Controller']['pid_type'], config['TempCtrl'],
heater.get_powers(), args.sim_warp_factor)
DT, theta_amb, config['TempCtrl']['pid_type'], config['TempCtrl'],
heater.get_powers(), args.sim_warp_factor, config.get('Pot', {}))
sud_task = SudTask(sud, tc, stirrer, pot, DT, DT_TASK, dispatcher.msgio_get("Sud"), forecast_estimator)
taskmgr.add(sud_task)
# Records every Sud run's measured data and forecast to their own
-4
View File
@@ -2,10 +2,6 @@
"Name": "Sud-0010",
"Description": "Rotfraenkisch, Dunkles Lager",
"pot": {
"mass": 5.96,
"material": "Edelstahl 18/10",
"L": 0.2,
"Td": 17,
"grain_mass": 0,
"water_mass": 22,
"volumen": 30