#!/usr/bin/env python3 """Store batch run results into a SQLite database. Reads the aggregate JSON report written by the batch runner and inserts all camera-level + summary data. Safe to run multiple times — uses (date, location, camera_id) as the unique key, so re-runs update existing rows instead of duplicating. Usage: python3 store_results.py /path/to/output/counts_2026-06-10.json --location kandang-atas python3 store_results.py /path/to/output/counts_2026-06-10.json --db /var/lib/chickens.db """ from __future__ import annotations import argparse import json import sqlite3 import sys from pathlib import Path CREATE_TABLE = """ CREATE TABLE IF NOT EXISTS batch_runs ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, location TEXT NOT NULL, camera_id TEXT NOT NULL, total_entered INTEGER NOT NULL DEFAULT 0, frames_processed INTEGER NOT NULL DEFAULT 0, elapsed_seconds REAL NOT NULL DEFAULT 0.0, stopped_reason TEXT NOT NULL DEFAULT '', source_video TEXT NOT NULL DEFAULT '', generated_at TEXT NOT NULL DEFAULT '', UNIQUE(date, location, camera_id) ) """ INSERT_SQL = """ INSERT INTO batch_runs (date, location, camera_id, total_entered, frames_processed, elapsed_seconds, stopped_reason, source_video, generated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(date, location, camera_id) DO UPDATE SET total_entered = excluded.total_entered, frames_processed = excluded.frames_processed, elapsed_seconds = excluded.elapsed_seconds, stopped_reason = excluded.stopped_reason, source_video = excluded.source_video, generated_at = excluded.generated_at """ SUMMARY_QUERY = """ SELECT date, location, COUNT(*) AS camera_count, SUM(total_entered) AS total_chickens, SUM(elapsed_seconds) AS total_seconds, ROUND(SUM(elapsed_seconds) / 60.0, 1) AS total_minutes FROM batch_runs WHERE date = ? AND location = ? GROUP BY date, location """ def store_report(report_path: str, location: str, db_path: str) -> None: with open(report_path) as f: report = json.load(f) date = report["date"] cameras = report["cameras"] generated_at = report.get("generated_at", "") Path(db_path).parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(db_path) conn.execute("PRAGMA journal_mode=WAL") conn.execute(CREATE_TABLE) rows = 0 for camera_id, entry in cameras.items(): if entry.get("skipped"): continue conn.execute(INSERT_SQL, ( date, location, camera_id, entry.get("total_entered", 0), entry.get("frames_processed", 0), entry.get("elapsed_seconds", 0), entry.get("stopped_reason", ""), entry.get("source_video", ""), generated_at, )) rows += 1 conn.commit() # print summary row = conn.execute(SUMMARY_QUERY, (date, location)).fetchone() if row: print(f"\n[db] {row[0]} | {row[1]} | {row[2]} cameras | " f"{row[3]} chickens | {row[4]:.0f}s ({row[5]} min)") # also print per-camera breakdown cur = conn.execute( "SELECT camera_id, total_entered, elapsed_seconds " "FROM batch_runs WHERE date=? AND location=? ORDER BY camera_id", (date, location)) for cam_id, count, secs in cur: print(f" {cam_id}: {count} chickens, {secs:.0f}s") conn.close() print(f"\n[db] wrote {rows} rows to {db_path}") def main(): parser = argparse.ArgumentParser( description="Store batch run results into SQLite") parser.add_argument("report", help="Path to counts_YYYY-MM-DD.json") parser.add_argument("--location", required=True, help="Location name (e.g. kandang-atas)") parser.add_argument("--db", default="chicken_counts.db", help="SQLite database path") args = parser.parse_args() if not Path(args.report).exists(): print(f"error: report not found: {args.report}", file=sys.stderr) sys.exit(1) store_report(args.report, args.location, args.db) if __name__ == "__main__": main()