From 6d1429daca737f54257c097015677a0d678ab21f Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Mon, 6 Jul 2026 10:45:55 +0200 Subject: [PATCH] feat: show status line earlier and count energy without a running Sud Both clients' status line only showed pot mass/water/energy once a Sud schedule was loaded and running, leaving it blank/minimal at connect time and while idle. Show it as early as possible instead, falling back to config.json's Pot section (mass/water_mass/volumen) when no schedule is loaded yet; step description/remaining-time still only appear once a schedule is actually active. web/app.js: updateStatusLine() now mirrors updatePotVisualization()'s existing sudEmpty ? potConfig... : sud... pattern, and adds a Volume figure that was previously only used for the pot-fill visualization. client/brewpi_gui.py: same treatment, plus new sud_volumen tracking (the desktop GUI had no volume concept at all before - added by reading the doc's own pot.volumen with a config fallback, since Sud._parse_data() doesn't return it). The System message's 'Pot' key now also refreshes the status label immediately so it doesn't wait for some unrelated message to trigger a repaint. tasks/sud.py: energy was only accumulated outside IDLE/DONE, so a manually-driven heater with no Sud loaded/started never counted toward the total shown in either client. Now counts in every state except DONE - IDLE banks into a stable index -1 that's never shown per-step (Progress tab only covers the schedule's own indices) but is included in the status line's summed total. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01M2ierBoxW3v7nUbDw3M2pE --- client/brewpi_gui.py | 78 +++++++++++++++++++++++++++----------------- tasks/sud.py | 15 ++++++--- web/app.js | 52 ++++++++++++++++------------- 3 files changed, 88 insertions(+), 57 deletions(-) diff --git a/client/brewpi_gui.py b/client/brewpi_gui.py index 54193d7..737ceec 100755 --- a/client/brewpi_gui.py +++ b/client/brewpi_gui.py @@ -457,6 +457,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.sud_pot_mass = 0 self.sud_grain_mass = 0 self.sud_water_mass = 0 + self.sud_volumen = 0 self.sud_step_index = None self.sud_step_descr = None self.sud_step_type = None @@ -745,6 +746,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.sud_pot_mass = 0 self.sud_grain_mass = 0 self.sud_water_mass = 0 + self.sud_volumen = 0 self.sud_step_index = None self.sud_step_descr = None self.sud_step_type = None @@ -787,6 +789,8 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.doubleSpinBox_pot_temp.setVisible(msg['PlantSim']) elif "Pot" in key: self.pot_config = msg['Pot'] + if self.sud_empty: + self.update_status_step_label() self.update_status_env_label() def on_action_pot_reset(self): @@ -1166,42 +1170,50 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self._update_step_plates() def update_status_step_label(self): - if self.sud_step_descr: - # +1: sud_step_index is the 0-based index Sud.index uses, but - # StepPlate (the Progress tab) labels steps 1-based - match it - # here so the status bar doesn't announce a step one number - # below what the Progress tab shows for the same step. - text = "Step {}: {}".format(self.sud_step_index + 1, self.sud_step_descr) - if self.sud_step_type == 'hold': - remaining = max(self.sud_hold_remaining, 0.0) - text += " ({:.0f}:{:02.0f} remaining)".format(remaining // 60, remaining % 60) - # sud_step_index is the same 0-based index Sud.index uses into - # its own schedule list (see components/sud.py) - self. - # sud_schedule is built from the very same doc, so it lines up - # directly; grain_mass/water_mass are already resolved against - # default.step there (see components/sud.py's _build_step()). - if 0 <= self.sud_step_index < len(self.sud_schedule): - step = self.sud_schedule[self.sud_step_index] - step_pot = step.get('pot', {}) - grain, water = step_pot.get('grain_mass', 0), step_pot.get('water_mass', 0) - else: - grain, water = self.sud_grain_mass, self.sud_water_mass - elif self.sud_schedule: - # Loaded but not started yet (no real Step message has + text = "" + # Before any schedule is loaded, fall back to config.json's Pot + # section (self.pot_config, captured in on_system_changed()) so the + # status bar already shows pot mass/water/volume/energy as soon as + # the server connects, rather than a blank status bar until Load - + # mirrors web/app.js's updateStatusLine(). Step description/ + # remaining-time only exist once a schedule is actually loaded, so + # they stay inside the "not empty" branch below. + if self.sud_empty: + pot = self.pot_config.get('mass', 0) + water = self.pot_config.get('water_mass', 0) + volumen = self.pot_config.get('volumen', 0) + grain = 0 + else: + pot = self.sud_pot_mass + volumen = self.sud_volumen + grain, water = self.sud_grain_mass, self.sud_water_mass + if self.sud_step_descr: + # +1: sud_step_index is the 0-based index Sud.index uses, but + # StepPlate (the Progress tab) labels steps 1-based - match it + # here so the status bar doesn't announce a step one number + # below what the Progress tab shows for the same step. + text = "Step {}: {}".format(self.sud_step_index + 1, self.sud_step_descr) + if self.sud_step_type == 'hold': + remaining = max(self.sud_hold_remaining, 0.0) + text += " ({:.0f}:{:02.0f} remaining)".format(remaining // 60, remaining % 60) + # sud_step_index is the same 0-based index Sud.index uses into + # its own schedule list (see components/sud.py) - self. + # sud_schedule is built from the very same doc, so it lines up + # directly; grain_mass/water_mass are already resolved against + # default.step there (see components/sud.py's _build_step()). + if 0 <= self.sud_step_index < len(self.sud_schedule): + step = self.sud_schedule[self.sud_step_index] + step_pot = step.get('pot', {}) + grain, water = step_pot.get('grain_mass', 0), step_pot.get('water_mass', 0) + # else: loaded but not started yet (no real Step message has # arrived) - preview the doc's own initial grain_mass/ # water_mass right away instead of leaving the status line # blank until Start, mirroring tasks/sud.py's SudTask. # apply_plant_params() applying it to the real plant/ # controller immediately on Load too. - text = "" - grain, water = self.sud_grain_mass, self.sud_water_mass - else: - self.statusBar().clearMessage() - return - pot = self.sud_pot_mass - mass_text = "Pot: {:.2f} kg, Water: {:.2f} kg, Grain {:.2f} kg, total {:.2f} kg".format( - pot, water, grain, pot + water + grain) + mass_text = "Pot: {:.2f} kg, Water: {:.2f} kg, Grain {:.2f} kg, total {:.2f} kg, Volume {:.1f} L".format( + pot, water, grain, pot + water + grain, volumen) 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_last (Sud's own tick-counted total - @@ -1251,6 +1263,12 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.sud_pot_mass = pot_mass self.sud_grain_mass = grain_mass self.sud_water_mass = water_mass + # Sud._parse_data() has no notion of 'volumen' (unlike mass/L/Td, + # nothing downstream needs it for the actual physics) - read it + # straight off the doc, falling back to config.json's Pot.volumen + # same as _parse_data() does for mass, mirroring web/app.js's + # parseSudDoc(). + self.sud_volumen = doc.get('pot', {}).get('volumen', self.pot_config.get('volumen', 0)) self.sud_empty = len(schedule) == 0 if is_new_schedule: self.sud_step_descr = None diff --git a/tasks/sud.py b/tasks/sud.py index f6badfc..43355ff 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -653,11 +653,16 @@ class SudTask(ATask): # wiring), in Watts; dt is this tick's simulated seconds, so # the product is Joules, accumulated for whichever step is # current (see on_step_changed()). Counted through every - # non-idle state, WAIT_USER/PAUSED included - the controller - # stays enabled (see on_state_changed()) and may well still - # be actively holding, drawing real power, even though the - # schedule itself isn't progressing. - if self.sud.state not in (SudState.IDLE, SudState.DONE): + # state except DONE, IDLE included - a manually-driven heater + # (no schedule loaded/started yet, e.g. pre-heating strike + # water by hand) still draws real power and should show up in + # the same total; self.sud.index is a stable -1 while IDLE + # (see components/sud.py's _reset_run_state()), so this banks + # into energy_by_step[-1] same as any other step once a real + # Start moves the index on - never shown per-step (Progress + # tab's step-plates only cover the schedule's own indices) but + # still included in the status line's summed total. + if self.sud.state != SudState.DONE: self.energy_step_accum_j += self.pot.get_power() * self.dt self._energy_changed.set(self.energy_step_accum_j / 3600.0) self.sud.tick(self.dt) diff --git a/web/app.js b/web/app.js index b5bd249..b61eb32 100644 --- a/web/app.js +++ b/web/app.js @@ -479,31 +479,39 @@ function updatePotVisualization() { function updateStatusLine() { const el = document.getElementById('sud-status-line'); - if (sudSchedule.length === 0) { - const w = potConfig.water_mass || 0; - el.textContent = w > 0 ? `No schedule – ${w} L water` : ''; - return; - } let text = ''; - let grain = sudGrainMass, water = sudWaterMass; - if (sudStepDescr) { - // +1: sudStepIndex is the 0-based index Sud.index uses, but the - // Progress tab's step-plates (createStepPlate()) label 1-based - - // match it here, same fix as client/brewpi_gui.py's - // update_status_step_label(). - text = `Step ${sudStepIndex + 1}: ${sudStepDescr}`; - if (sudStepType === 'hold') { - const remaining = Math.max(sudHoldRemaining, 0); - text += ` (${Math.floor(remaining / 60)}:${String(Math.floor(remaining % 60)).padStart(2, '0')} remaining)`; - } - if (sudStepIndex !== null && sudStepIndex >= 0 && sudStepIndex < sudSchedule.length) { - const step = sudSchedule[sudStepIndex]; - grain = step.grain_mass || 0; - water = step.water_mass || 0; + // Before any schedule is loaded, fall back to config.json's Pot + // section (via potConfig) so the status line already shows pot mass/ + // water/volume/energy as soon as the System message arrives - same + // sudEmpty ? potConfig... : sud... pattern as updatePotVisualization() + // above. Step description/remaining-time only exist once a schedule + // is actually loaded, so they stay inside the sudEmpty===false branch. + let grain = 0, water = potConfig.water_mass || 0; + let pot = potConfig.mass || 0; + let volumen = potConfig.volumen || 0; + if (!sudEmpty) { + grain = sudGrainMass; + water = sudWaterMass; + pot = sudPotMass; + volumen = sudVolumen; + if (sudStepDescr) { + // +1: sudStepIndex is the 0-based index Sud.index uses, but the + // Progress tab's step-plates (createStepPlate()) label 1-based - + // match it here, same fix as client/brewpi_gui.py's + // update_status_step_label(). + text = `Step ${sudStepIndex + 1}: ${sudStepDescr}`; + if (sudStepType === 'hold') { + const remaining = Math.max(sudHoldRemaining, 0); + text += ` (${Math.floor(remaining / 60)}:${String(Math.floor(remaining % 60)).padStart(2, '0')} remaining)`; + } + if (sudStepIndex !== null && sudStepIndex >= 0 && sudStepIndex < sudSchedule.length) { + const step = sudSchedule[sudStepIndex]; + grain = step.grain_mass || 0; + water = step.water_mass || 0; + } } } - const pot = sudPotMass; - const massText = `Pot: ${pot.toFixed(2)} kg, Water: ${water.toFixed(2)} kg, Grain ${grain.toFixed(2)} kg, total ${(pot + water + grain).toFixed(2)} kg`; + const massText = `Pot: ${pot.toFixed(2)} kg, Water: ${water.toFixed(2)} kg, Grain ${grain.toFixed(2)} kg, total ${(pot + water + grain).toFixed(2)} kg, Volume ${volumen.toFixed(1)} L`; text = text ? `${text} ${massText}` : massText; const elapsedText = `Elapsed ${elapsed !== null ? formatDuration(elapsed) : '-:--:--'}`; const totalEnergyKwh = (Object.values(energyByStep).reduce((a, b) => a + b, 0) + energyCurrent) / 1000.0;