Files
brewpi/tasks/server_log.py
T
jensandClaude Sonnet 5 0e60860cf0 feat: periodically checkpoint server/sud logs to disk
Both loggers previously only wrote their JSON log on their end trigger
(shutdown / Stop-or-DONE), losing the whole session/run's data on a
hard crash or power loss. Add a log_interval (config.json, default
300s) that rewrites the same file whole every N seconds in between.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvgC7oy9MxaA4ZQxXqkNdS
2026-07-01 09:44:11 +02:00

86 lines
2.7 KiB
Python

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', config=None, dt=None, log_interval=None):
ATask.__init__(self, interval)
self.tc = tc
self.heater = heater
self.heater_task = heater_task
self.path = path
self.config = config
self.dt = dt
self.log_interval = log_interval
self._samples = []
self._param_events = []
self._t0 = time.monotonic()
self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime())
self._last_write = self._t0
def log_plant_params(self, elapsed, params):
self._param_events.append({'t': time.monotonic() - self._t0, 'params': params})
async def on_process(self):
while True:
if self._should_sample():
self._samples.append(self._take_sample())
# Periodically checkpoints to disk (same file, overwritten whole
# each time - see write()) so a hard crash/power loss only loses
# up to log_interval seconds, not the whole session/run.
now = time.monotonic()
if self.log_interval and now - self._last_write >= self.log_interval:
self.write()
self._last_write = now
await asyncio.sleep(self.interval)
def _should_sample(self):
return True
def _take_sample(self):
power_set = max(self.heater_task.power_soll, self.heater_task.power_actor)
return {
'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,
}
def _filename(self):
return 'log_{}.json'.format(self._run_id)
def write(self):
if not self._samples:
return
os.makedirs(self.path, exist_ok=True)
filename = self._filename()
path = os.path.join(self.path, filename)
log_data = {'Name': ''}
if self.dt is not None:
log_data['dt'] = self.dt
if self.config is not None:
log_data['Config'] = self.config
if self._param_events:
log_data['PlantParams'] = self._param_events
log_data['Samples'] = self._samples
with open(path, 'w') as f:
json.dump(log_data, f, indent='\t')
print('Server log: wrote {} samples to {}'.format(len(self._samples), filename))