log: Sud lifecycle transitions and stirrer/heater connect state

Logs load/start/pause/continue/stop/entering-step/wait-user/finished
on Sud (components/sud.py) and connected/disconnected on
StirrerTask/HeaterTask, using the self.log added earlier.

SudForecastEstimator.estimate() (components/sud_forecast.py) drives its
own throwaway Sud through an entire schedule in a tight loop (no real
waiting) to predict its duration, and re-runs that on every real step
transition too - since it's still a Sud, it logged through the exact
same "Sud" logger as the real, server-driven one, making an instant
internal forecast run indistinguishable in the log from an actual
brew. Gave it its own "SudForecastEstimator" logger, silenced to
WARNING by default, so only the real Sud's transitions show up.
This commit is contained in:
2026-07-10 22:28:01 +02:00
parent a1864a5257
commit ce5eb3bc23
4 changed files with 20 additions and 0 deletions
+8
View File
@@ -154,6 +154,7 @@ class Sud(AttributeChange):
self._data = data
self._reset_run_state()
self.log.info("Loaded '{}'".format(self.name))
return True
def derive_plant_params(self, grain_mass, water_mass):
@@ -183,11 +184,13 @@ class Sud(AttributeChange):
if self.state == SudState.PAUSED:
self.state = self._paused_from
self._paused_from = None
self.log.info("Continued")
return
if self.state not in (SudState.IDLE, SudState.DONE):
return
self.index = -1
self.elapsed = 0.0
self.log.info("Started")
self._advance()
def pause(self):
@@ -196,11 +199,13 @@ class Sud(AttributeChange):
if self.state in (SudState.RAMPING, SudState.HOLDING):
self._paused_from = self.state
self.state = SudState.PAUSED
self.log.info("Paused")
def stop(self):
"""Aborts the run, discarding progress back to IDLE."""
if self.state not in (SudState.IDLE, SudState.DONE):
self._reset_run_state()
self.log.info("Stopped")
def confirm(self):
if self.state == SudState.WAIT_USER:
@@ -243,6 +248,7 @@ class Sud(AttributeChange):
self.state = SudState.DONE
self.user_message = None
self.step = None
self.log.info("Finished")
return
next_step = self.schedule[self.index]
@@ -254,9 +260,11 @@ class Sud(AttributeChange):
self.state = SudState.RAMPING
self.user_message = next_step.get('user_message')
self.step = next_step
self.log.info("Entering step {} ({})".format(next_step['number'], next_step.get('descr')))
def _finish_step(self):
if self.step.get('user_wait_for_continue', False):
self.state = SudState.WAIT_USER
self.log.info("Waiting for user interaction")
else:
self._advance()
+10
View File
@@ -1,3 +1,4 @@
import logging
from components.plant import Pot
from components.pid import PidFactory
from components.sud import Sud, SudState
@@ -7,6 +8,14 @@ from components.sud import Sud, SudState
# simulation forever - the estimate is simply cut off there.
MAX_TICKS = 200000
# estimate() below drives a throwaway Sud through an entire schedule in a
# tight loop (no real waiting) purely to predict its duration - since it's
# still a Sud, it would otherwise log through the exact same "Sud" logger
# the real, server-driven one uses (components/sud.py's self.log), making
# an instant internal forecast run indistinguishable in the log from an
# actual brew. Quiet by default; raise this logger's own level to see it.
logging.getLogger('SudForecastEstimator').setLevel(logging.WARNING)
# Mirrors tasks/heater.py's own pulse_period_s - HeaterTask duty-cycles its
# device's discrete power steps over a rolling window this many *real*
# seconds wide, regardless of warp factor (see estimate()'s actuate()
@@ -95,6 +104,7 @@ class SudForecastEstimator:
start_theta = self.theta_amb
sud = Sud(self.pot_config)
sud.log = logging.getLogger('SudForecastEstimator')
if not sud.load(doc) or not sud.schedule:
return [0.0], [start_theta], SudState.DONE, {}
+1
View File
@@ -33,6 +33,7 @@ class HeaterTask(ATask):
self._on_connected_changed = callback
def on_connected_changed(self, value):
self.log.info("Connected" if value else "Disconnected")
fire_and_forget(self.send({'Connected': value}))
if self._on_connected_changed:
self._on_connected_changed(value)
+1
View File
@@ -33,6 +33,7 @@ class StirrerTask(ATask):
self._on_connected_changed = callback
def on_connected_changed(self, value):
self.log.info("Connected" if value else "Disconnected")
fire_and_forget(self.send({'Connected': value}))
if self._on_connected_changed:
self._on_connected_changed(value)