analyze_log: accept a direct log path as single argument

The two-argument form (date_time sud_name) is unchanged.  When called
with a single argument it is treated as a direct path to any log_*.json
(e.g. a server-session log without a Sud name); the forecast panel is
shown only if a matching forecast_*.json exists alongside it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 19:11:31 +02:00
co-authored by Claude Sonnet 4.6
parent 4f1e05bab1
commit 9be0e49a63
+24 -13
View File
@@ -1,14 +1,16 @@
#!/usr/bin/env python3
"""Offline analysis of a log/forecast pair produced by the BrewPi server.
"""Offline analysis of a log (and optional forecast) produced by the BrewPi server.
Usage:
python analyze_log.py <date_time> <sud_name> [--log-dir <dir>]
python analyze_log.py <path/to/log_*.json>
Example:
python analyze_log.py 20260628T184903 Sud-0010
The two-argument form loads logs/<date_time>_<sud_name>.json and its paired
forecast file. The single-argument form accepts a direct path to any
log_*.json (e.g. a server-session log without a Sud name); the forecast panel
is shown only if a matching forecast_*.json exists alongside it.
Loads logs/<date_time>_{sud_name}.json (log and forecast) and renders the same
two plots the PyQt GUI shows: the 3-panel realtime strip chart and the
Renders the 3-panel realtime strip chart and, when available, the
forecast-vs-actual comparison.
"""
@@ -102,32 +104,41 @@ def plot_forecast(forecast, samples, name):
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('date_time', help='Timestamp part of the filename, e.g. 20260628T184903')
parser.add_argument('sud_name', help='Sud name part of the filename, e.g. Sud-0010')
parser.add_argument('date_time_or_path',
help='Timestamp part of the filename (e.g. 20260628T184903) '
'or a direct path to a log_*.json file')
parser.add_argument('sud_name', nargs='?', default=None,
help='Sud name part of the filename (e.g. Sud-0010); '
'omit when passing a direct file path')
default_log_dir = Path(__file__).parent.parent / 'logs'
parser.add_argument('--log-dir', default=str(default_log_dir),
help='Directory containing the log files (default: %(default)s)')
args = parser.parse_args()
if args.sud_name is None:
log_path = Path(args.date_time_or_path)
stem = log_path.stem[len('log_'):] if log_path.stem.startswith('log_') else log_path.stem
forecast_path = log_path.parent / 'forecast_{}.json'.format(stem)
else:
log_dir = Path(args.log_dir)
stem = '{}_{}'.format(args.date_time, args.sud_name)
stem = '{}_{}'.format(args.date_time_or_path, args.sud_name)
log_path = log_dir / 'log_{}.json'.format(stem)
forecast_path = log_dir / 'forecast_{}.json'.format(stem)
for path in (log_path, forecast_path):
if not path.exists():
print('Error: file not found: {}'.format(path), file=sys.stderr)
if not log_path.exists():
print('Error: file not found: {}'.format(log_path), file=sys.stderr)
sys.exit(1)
with open(log_path) as f:
log = json.load(f)
with open(forecast_path) as f:
forecast = json.load(f)
name = log.get('Name', stem)
samples = log['Samples']
plot_realtime(samples, name)
if forecast_path.exists():
with open(forecast_path) as f:
forecast = json.load(f)
plot_forecast(forecast, samples, name)
plt.show()