Use the server's simulated forecast for the dynamic remainder too
SudForecastPlot.show_dynamic()'s dashed "remaining" line was still
using the naive abs(delta)/rate estimate even after a run started,
even though the (much more accurate) server-side
SudForecastEstimator was already available - the static forecast got
the upgrade, the live one didn't.
tasks/sud.py: SudTask now recomputes a forecast for just the remaining
steps (seeded from the live current temperature) on every step change
and sends it as {'RemainingForecast': {...}}. Building the synthetic
"rest of this step" entry for a step currently HOLDING needs to keep
the current step's grain_mass/water_mass/temperature/etc - a bare
{'hold': {...}} re-resolves those to None against the synthetic doc's
empty default.step, breaking derive_plant_params() (caught live: a
TypeError on every step change, silently swallowed by asyncio).
client/brewpi_gui.py: on_plot_timer() now uses the server's
RemainingForecast for the dashed projection whenever available,
falling back to the naive estimate only for the brief window before
each step's recomputation arrives. show_dynamic() takes the already-
computed (t_rem, theta_rem) instead of computing it internally, so the
GUI/server sources are interchangeable from its point of view.
This commit is contained in:
@@ -84,6 +84,7 @@ class SudTask(ATask):
|
||||
'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,
|
||||
}}))
|
||||
asyncio.create_task(self.send_remaining_forecast())
|
||||
|
||||
def on_state_changed(self, value):
|
||||
asyncio.create_task(self.send({'State': str(value)}))
|
||||
@@ -117,6 +118,59 @@ class SudTask(ATask):
|
||||
t, theta = await loop.run_in_executor(None, self.forecast_estimator.estimate, doc)
|
||||
await self.send({'Forecast': {'T': t, 'Theta': theta}})
|
||||
|
||||
def remaining_schedule(self):
|
||||
"""The not-yet-done part of the running schedule - the current
|
||||
step's already-finished phase is dropped, and its still-to-run
|
||||
phase is left for SudForecastEstimator to size from the live
|
||||
start_theta/hold_remaining it's seeded with, rather than the
|
||||
step's nominal start. Mirrors client/brewpi_gui.py's
|
||||
Window._remaining_schedule(), which does the same thing
|
||||
client-side for its own (less accurate) naive fallback."""
|
||||
schedule = self.sud.schedule
|
||||
index = self.sud.index
|
||||
if not schedule or not (0 <= index < len(schedule)):
|
||||
return []
|
||||
current = schedule[index]
|
||||
rest = schedule[index + 1:]
|
||||
if self.sud.state == SudState.WAIT_USER:
|
||||
return rest
|
||||
if self.sud.state == SudState.HOLDING:
|
||||
# Keep the rest of the current (already fully-resolved) step -
|
||||
# grain_mass/water_mass/temperature/etc. - SudForecastEstimator
|
||||
# re-resolves each step against an empty default.step, so a
|
||||
# bare {'hold': {...}} here would leave those None instead of
|
||||
# inherited, breaking derive_plant_params(). Only the ramp
|
||||
# phase is actually done; drop it and override the hold's
|
||||
# duration to what's actually left.
|
||||
remaining_minutes = max(self.sud.hold_remaining, 0.0) / 60.0
|
||||
synthetic = {k: v for k, v in current.items() if k != 'ramp'}
|
||||
synthetic['hold'] = {**current.get('hold', {}), 'duration': remaining_minutes}
|
||||
return [synthetic] + rest
|
||||
return [current] + rest
|
||||
|
||||
async def send_remaining_forecast(self):
|
||||
"""Like send_forecast(), but for the remaining steps only, seeded
|
||||
from the live current temperature - the dynamic forecast's
|
||||
projected-remainder line (GUI's SudForecastPlot.line_projected)
|
||||
uses this instead of its own naive abs(delta)/rate estimate
|
||||
whenever it's available."""
|
||||
if self.forecast_estimator is None:
|
||||
return
|
||||
remaining = self.remaining_schedule()
|
||||
if not remaining:
|
||||
return
|
||||
doc = {
|
||||
'Name': self.sud.name,
|
||||
'Description': self.sud.description,
|
||||
'pot_mass': self.sud.pot_mass,
|
||||
'pot_material': self.sud.pot_material,
|
||||
'steps': remaining,
|
||||
}
|
||||
start_theta = self.tc.get_theta_ist()
|
||||
loop = asyncio.get_event_loop()
|
||||
t, theta = await loop.run_in_executor(None, self.forecast_estimator.estimate, doc, start_theta)
|
||||
await self.send({'RemainingForecast': {'T': t, 'Theta': theta}})
|
||||
|
||||
async def recv(self, data):
|
||||
for pair in data.items():
|
||||
if 'Start' in pair[0]:
|
||||
|
||||
Reference in New Issue
Block a user