From 5751d268733c76c7b37ec51c90e3d223ce3a7d06 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Tue, 30 Jun 2026 18:26:28 +0200 Subject: [PATCH] server: graceful Ctrl-C shutdown; HendiCtrl closes connection cleanly On KeyboardInterrupt, brewpi.py cancels all asyncio tasks (letting HeaterTask's `with device.open()` finally block run), then calls heater.close() as belt-and-suspenders before closing the event loop. On shutdown HeaterHendi.activate(False) now sets the switch to 0 instead of disabling remote control, so the Hendi stays in remote mode at 0 W rather than dropping back to its local panel state. HendiCtrl.close() does the same (setSwitch(0)) then closes the serial port. Co-Authored-By: Claude Sonnet 4.6 --- components/actor/heater_hendi.py | 5 ++++- components/actor/hendiCtrl.py | 7 +++++++ components/aheater.py | 3 +++ server/brewpi.py | 20 +++++++++++++++++++- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/components/actor/heater_hendi.py b/components/actor/heater_hendi.py index 520021f..f8bd472 100755 --- a/components/actor/heater_hendi.py +++ b/components/actor/heater_hendi.py @@ -24,7 +24,7 @@ class HeaterHendi(AHeater): if enable: self.hendi.remoteEnable(1) else: - self.hendi.remoteEnable(0) + self.hendi.setSwitch(0) def is_activated(self): s = self.hendi.isRemoteEnable() @@ -44,3 +44,6 @@ class HeaterHendi(AHeater): def get_power(self): return self.power_eff + + def close(self): + self.hendi.close() diff --git a/components/actor/hendiCtrl.py b/components/actor/hendiCtrl.py index 88da308..cc4fce4 100755 --- a/components/actor/hendiCtrl.py +++ b/components/actor/hendiCtrl.py @@ -183,6 +183,13 @@ class HendiCtrl: def getPowerWatts(self): return int(self.toWatts(self.getPowerDigits()) + 0.5) + def close(self): + try: + self.setSwitch(0) + except Exception: + pass + self.ser.close() + if __name__ == '__main__': diff --git a/components/aheater.py b/components/aheater.py index a3ced03..79e2451 100644 --- a/components/aheater.py +++ b/components/aheater.py @@ -20,6 +20,9 @@ class AHeater(AttributeChange): def get_powers(self): pass + def close(self): + pass + @contextmanager def open(self): try: diff --git a/server/brewpi.py b/server/brewpi.py index 83e5823..d63e678 100755 --- a/server/brewpi.py +++ b/server/brewpi.py @@ -190,4 +190,22 @@ if __name__ == '__main__': threading.Thread(target=http_server.serve_forever, daemon=True).start() print("Serving {} on http://0.0.0.0:{}".format(web_dir, args.http_port)) - server.run_forever() + loop = asyncio.get_event_loop() + try: + loop.run_forever() + except KeyboardInterrupt: + print("\nShutting down...") + finally: + # Cancel all tasks so their finally blocks (e.g. HeaterTask's + # `with device.open()`) get a chance to run before we close the port. + pending = asyncio.all_tasks(loop) + for task in pending: + task.cancel() + if pending: + loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) + # Belt-and-suspenders: explicitly close the heater connection even if + # the task cleanup above failed to reach HeaterHendi.activate(False). + heater.close() + http_server.shutdown() + loop.close() + print("Server stopped.")