From 9f221c5d19df87d8e1953e0aca88598b23967c9d Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Wed, 1 Jul 2026 00:43:55 +0200 Subject: [PATCH] fix: re-enable closed-loop on Sud start regardless of client HeaterTask.shutdown() switched the heater to open-loop when a brew ended, but nothing switched it back when a new Sud was started. The browser happened to send {ClosedLoop: true} as part of its UI setup, so its connect-and-start path worked; PyQt (and any other client that doesn't send that message) left the heater in open-loop and the TC never drove it. Add SudTask._on_start / set_on_start() hook, HeaterTask.start_closed_loop() (mirror of shutdown()), and wire them in brewpi.py so the server switches to closed-loop and broadcasts the new state to all clients the moment any client starts a brew. Co-Authored-By: Claude Sonnet 4.6 --- server/brewpi.py | 3 +++ tasks/heater.py | 8 ++++++++ tasks/sud.py | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/server/brewpi.py b/server/brewpi.py index 74c43f4..e5d4685 100755 --- a/server/brewpi.py +++ b/server/brewpi.py @@ -143,6 +143,9 @@ if __name__ == '__main__': tc.set_on_changed("y", heater_task.actor) # HeaterTask owns TC enable: enabled in closed-loop mode, disabled in open-loop. heater_task.set_on_closed_loop_changed(tc.set_enabled) + # Brew start: switch heater to closed-loop so the TC drives it regardless + # of which client (browser, PyQt GUI, …) started the brew. + sud_task.set_on_start(heater_task.start_closed_loop) # Brew end (DONE/IDLE): shut heater off and broadcast open-loop/power-0 to clients. sud_task.set_on_end(heater_task.shutdown) diff --git a/tasks/heater.py b/tasks/heater.py index 09b4e07..175bcf0 100755 --- a/tasks/heater.py +++ b/tasks/heater.py @@ -34,6 +34,14 @@ class HeaterTask(ATask): asyncio.create_task(self.send({'ClosedLoop': False})) self.power_set_changed(0) + def start_closed_loop(self): + """Called when a Sud starts. Re-enables closed-loop so the TC drives + the heater regardless of which client started the brew.""" + self.closed_loop = True + if self._on_closed_loop_changed: + self._on_closed_loop_changed(True) + asyncio.create_task(self.send({'ClosedLoop': True})) + def actor(self, y): self.power_actor = max(0, self.device.get_power_max() * y) diff --git a/tasks/sud.py b/tasks/sud.py index 38083cc..45a94ca 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -111,6 +111,7 @@ class SudTask(ATask): # when the current step has temperature=None (inherits from a prior step). self._paused_temp_soll = None self._on_end = None + self._on_start = None self._on_plant_params = None msg_handler.set_recv_handler(self.recv) @@ -229,6 +230,9 @@ class SudTask(ATask): def set_on_end(self, callback): self._on_end = callback + def set_on_start(self, callback): + self._on_start = callback + def on_state_changed(self, value): asyncio.create_task(self.send({'State': str(value)})) @@ -450,6 +454,8 @@ class SudTask(ATask): self.energy_by_step = {} self._energy_index = None self.sud.start() + if self._on_start: + self._on_start() if fresh_start: asyncio.create_task(self.send_forecast(self.sud.save())) else: