server: always write log_{date_time}.json for the full server session

Adds ServerLogTask (tasks/server_log.py) which records temp/power samples
from startup to shutdown regardless of Sud state, in the same Name/Samples
JSON format as SudLogTask so analyze_log.py and replay_sim.py can consume
it directly.  The file is written on graceful exit (Ctrl-C) from brewpi.py's
finally block, just before the heater is closed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 19:04:37 +02:00
co-authored by Claude Sonnet 4.6
parent 801e9809a6
commit 4f1e05bab1
3 changed files with 60 additions and 1 deletions
+7 -1
View File
@@ -15,7 +15,7 @@ from components.plant import PlantFactory
from components.actor import StirrerFactory from components.actor import StirrerFactory
from components.sud import Sud from components.sud import Sud
from components.sud_forecast import SudForecastEstimator from components.sud_forecast import SudForecastEstimator
from tasks import TaskManager, TempSensorTask, HeaterTask, PotTask, TcTask, StirrerTask, SudTask, SudLogTask from tasks import TaskManager, TempSensorTask, HeaterTask, PotTask, TcTask, StirrerTask, SudTask, SudLogTask, ServerLogTask
import argparse as ap import argparse as ap
@@ -140,6 +140,11 @@ if __name__ == '__main__':
# tasks/sud_log.py. # tasks/sud_log.py.
taskmgr.add(SudLogTask(sud, tc, heater, heater_task, sud_task, DT_TASK)) taskmgr.add(SudLogTask(sud, tc, heater, heater_task, sud_task, DT_TASK))
# Continuous server-session log - records from startup to shutdown
# regardless of Sud state; written to logs/log_{date_time}.json on exit.
server_log_task = ServerLogTask(tc, heater, heater_task, DT_TASK, args.logdir)
taskmgr.add(server_log_task)
# Assign data flow # Assign data flow
# Assign tc control value to heater # Assign tc control value to heater
tc.set_on_changed("y", heater_task.actor) tc.set_on_changed("y", heater_task.actor)
@@ -203,6 +208,7 @@ if __name__ == '__main__':
task.cancel() task.cancel()
if pending: if pending:
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True)) loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
server_log_task.write()
# Belt-and-suspenders: explicitly close the heater connection even if # Belt-and-suspenders: explicitly close the heater connection even if
# the task cleanup above failed to reach HeaterHendi.activate(False). # the task cleanup above failed to reach HeaterHendi.activate(False).
heater.close() heater.close()
+1
View File
@@ -6,3 +6,4 @@ from tasks.pot import PotTask
from tasks.tempctrl import TcTask from tasks.tempctrl import TcTask
from tasks.sud import SudTask from tasks.sud import SudTask
from tasks.sud_log import SudLogTask from tasks.sud_log import SudLogTask
from tasks.server_log import ServerLogTask
+52
View File
@@ -0,0 +1,52 @@
import asyncio
import json
import os
import time
from tasks import ATask
from components import APid, AHeater
class ServerLogTask(ATask):
"""Continuously records temp/power samples from server start to shutdown.
Writes a single log_{date_time}.json on close() in the same format as
SudLogTask (Name/Samples) so analyze_log.py and replay_sim.py can
consume it directly. Unlike SudLogTask, which only records during an
active Sud run, this log covers the full server session regardless of
Sud state — useful for verifying controller and heater behaviour
outside of a scheduled brew."""
def __init__(self, tc: APid, heater: AHeater, heater_task, interval, path='./logs'):
ATask.__init__(self, interval)
self.tc = tc
self.heater = heater
self.heater_task = heater_task
self.path = path
self._samples = []
self._t0 = time.monotonic()
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
async def on_process(self):
while True:
power_set = max(self.heater_task.power_soll, self.heater_task.power_actor)
self._samples.append({
't': time.monotonic() - self._t0,
'timestamp': time.time(),
'temp_ist': self.tc.theta_ist,
'temp_soll': self.tc.theta_soll_set,
'rate_ist': self.tc.heatrate_ist,
'rate_soll': self.tc.heatrate_soll,
'power_set': power_set,
'power_eff': self.heater.power_eff,
})
await asyncio.sleep(self.interval)
def write(self):
if not self._samples:
return
os.makedirs(self.path, exist_ok=True)
filename = 'log_{}.json'.format(self._run_id)
path = os.path.join(self.path, filename)
with open(path, 'w') as f:
json.dump({'Name': '', 'Samples': self._samples}, f, indent='\t')
print('Server log: wrote {} samples to {}'.format(len(self._samples), filename))