From 2913b41cab46faf89b44d0da1927d44697a55b52 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Wed, 24 Jun 2026 19:39:03 +0200 Subject: [PATCH] Fix elapsed showing --:-- at DONE, and energy not reset on restart sud_elapsed_seconds is intentionally reset to None once a run leaves a running state (it gates the forecast plot's dynamic-vs-static view switch) - but the status bar/Progress tab reused it too, so they lost the final total exactly when it mattered most: right as the run finished. Added sud_elapsed_last, which only ever gets overwritten by a real 'Elapsed' push and is reset on a fresh Start/Load, for those two displays to use instead. Separately, a fresh Start (restarting an already-loaded, now IDLE/ DONE schedule) never sent a new Load, so SudTask's energy bookkeeping - previously only reset in the Load handler - kept showing the previous run's banked totals until each step was revisited. Now also reset on a fresh Start (not a Pause->resume, which should keep it). Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_019qvu5giu7gvRCyEWzf2Vpx --- client/brewpi_gui.py | 56 ++++++++++++++++++++++++++++++++++++-------- tasks/sud.py | 8 +++++++ 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/client/brewpi_gui.py b/client/brewpi_gui.py index 8835ba0..1d4be02 100755 --- a/client/brewpi_gui.py +++ b/client/brewpi_gui.py @@ -438,6 +438,16 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): # load, which used to make the live trace visibly drift from the # forecast. self.sud_elapsed_seconds = None + # The last real 'Elapsed' value seen, full stop - unlike + # sud_elapsed_seconds above, never reset back to None once the run + # leaves a running state (that's specifically what the forecast + # plot's dynamic-vs-static view switch needs - see _elapsed_min()), + # so the status bar/Progress tab can still show where the run + # actually ended up rather than "--:--" the moment it finishes. + # Reset on a fresh Start/Load (see on_sud_changed()/ + # update_sud_forecast()) - a new run's total shouldn't open by + # showing the previous one's. + self.sud_elapsed_last = None self.sud_name = '' self.sud_schedule = [] self.sud_pot_mass = 0 @@ -1039,6 +1049,19 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): if self.sud_state in SUD_RUNNING_STATES and self.sud_elapsed_seconds is None: self.sud_elapsed_seconds = 0.0 + self.sud_elapsed_last = 0.0 + # A genuine fresh start (excludes Pause->resume, which + # never hits this branch - sud_elapsed_seconds is + # already non-None throughout a pause) means whatever + # energy the previous run banked belongs to that run, + # not this one - mirrors tasks/sud.py's SudTask.recv() + # resetting its own server-side bookkeeping on the same + # event. The server will overwrite these with the + # now-reset real figures within a tick or two regardless, + # this just avoids a stale flash of the old run's totals + # in between. + self.sud_energy_by_step = {} + self.sud_energy_current = 0.0 # Start the dynamic forecast's measured-history trace # fresh for this run. self.forecast_history = [] @@ -1093,6 +1116,13 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.sud_hold_remaining = msg['HoldRemaining'] self.update_status_step_label() elif "Elapsed" in key: + # Unlike sud_elapsed_seconds below, kept regardless of + # dynamic-view mode - see its own comment for why - so the + # status bar/Progress tab still have the real final total + # once a run finishes rather than "--:--". + self.sud_elapsed_last = msg['Elapsed'] + self._update_step_plates() + self.update_status_step_label() # Sud.elapsed resets to 0 on every fresh Load too (not just # on an actual run start - see components/sud.py's # _reset_run_state()) and broadcasts unconditionally - only @@ -1115,8 +1145,6 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): # at x=0. if not self.forecast_history and self.plot_temp_ist and msg['Elapsed'] < FRESH_RUN_ELAPSED_THRESHOLD_S: self.forecast_history.append((msg['Elapsed'] / 60.0, self.plot_temp_ist)) - self._update_step_plates() - self.update_status_step_label() elif "Forecast" in key: self.on_sud_forecast_received(msg['Forecast']) elif "Energy" in key: @@ -1185,15 +1213,17 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): pot, water, grain, pot + water + grain) text = text + " " + mass_text if text else mass_text # Sums, not just the active step's own figures: total process time - # is just self.sud_elapsed_seconds (Sud's own tick-counted total - + # is just self.sud_elapsed_last (Sud's own tick-counted total - # every step's time, including any WAIT_USER dwell, already adds - # up into it sequentially with no gaps); total energy has no such - # single running counter server-side (see tasks/sud.py's SudTask - - # energy is banked per step, not accumulated as one grand total), - # so it's summed here from every finished step's own Wh total - # plus whatever the currently active one has used so far. + # up into it sequentially with no gaps) - sud_elapsed_seconds + # itself would go back to "--:--" the moment a run finishes, see + # its own comment, which is exactly when this total matters most. + # Total energy has no such single running counter server-side + # (see tasks/sud.py's SudTask - energy is banked per step, not + # accumulated as one grand total), so it's summed here from every + # finished step's own Wh total plus the currently active one. elapsed_text = "Elapsed {}".format( - _format_duration(self.sud_elapsed_seconds) if self.sud_elapsed_seconds is not None else "--:--") + _format_duration(self.sud_elapsed_last) if self.sud_elapsed_last is not None else "--:--") total_energy_kwh = (sum(self.sud_energy_by_step.values()) + self.sud_energy_current) / 1000.0 energy_text = "Energy {:.2f} kWh".format(total_energy_kwh) text = "{} {} {}".format(text, elapsed_text, energy_text) @@ -1245,6 +1275,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.step_actual_frozen = {} self.sud_energy_by_step = {} self.sud_energy_current = 0.0 + self.sud_elapsed_last = None self._rebuild_step_plates() self.update_sud_actions() self.update_status_step_label() @@ -1287,7 +1318,12 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): mash schedule is at most a handful of steps.""" if not self.step_plates: return - current_elapsed = self.sud_elapsed_seconds if self.sud_elapsed_seconds is not None else 0.0 + # sud_elapsed_last, not sud_elapsed_seconds - the latter resets to + # None the moment a run finishes (see its own comment), which + # would otherwise make every finished step's remaining time jump + # back to its full duration right when the run ends instead of + # staying at 0:00. + current_elapsed = self.sud_elapsed_last if self.sud_elapsed_last is not None else 0.0 starts = self.sud_forecast_step_starts n = len(self.step_plates) for i, plate in enumerate(self.step_plates): diff --git a/tasks/sud.py b/tasks/sud.py index a996787..d4b1bd7 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -430,6 +430,14 @@ class SudTask(ATask): # whatever temperature existed back at Load (time passed, # possibly manual heating in between). fresh_start = self.sud.state in (SudState.IDLE, SudState.DONE) + if fresh_start: + # Belongs to the run that just ended (Stop, or running + # the schedule through to DONE), not the one about to + # begin - a Pause->resume (fresh_start False) keeps it, + # same as the forecast above. + self.energy_step_accum_j = 0.0 + self.energy_by_step = {} + self._energy_index = None self.sud.start() if fresh_start: asyncio.create_task(self.send_forecast(self.sud.save()))