diff --git a/API.md b/API.md new file mode 100644 index 0000000..c6642a8 --- /dev/null +++ b/API.md @@ -0,0 +1,149 @@ +# Chicken Counter API + +Base URL: `http://:8080` + +## Live Dashboard + +### `GET /` +Returns the dashboard HTML page. + +### `GET /api/cameras` +List cameras currently writing to `/dev/shm`. + +```json +{"cameras": ["CC1", "CC2", "CC3"]} +``` + +### `GET /shm//stats.json` +Live stats for the active pipeline. + +```json +{ + "frame_index": 5120, + "inside_box_count": 42, + "total_entered_count": 1858, + "track_count": 99, + "backward_active": false, + "smoothed_speed": 4.3, + "count_events": 0, + "run_date": "2026-06-10" +} +``` + +### `GET /shm//frame.jpg` +Live JPEG frame from the active pipeline. + +--- + +## Database + +All endpoints require the dashboard to be started with `--db `. If no DB exists, endpoints return `[]` or `{}`. + +### `GET /api/db/summary` +Overall totals across all dates and locations. + +```json +{ + "days": 12, + "locations": 2, + "total_runs": 48, + "total_chickens": 125000, + "total_hours": 8.5 +} +``` + +### `GET /api/db/history` +Per-date summary, newest first (max 50 rows). + +```json +[ + { + "date": "2026-06-10", + "location": "kandang-atas", + "cams": 4, + "total": 5570, + "minutes": 40.2 + } +] +``` + +### `GET /api/db/date/` +Detail for a specific date. Format: `YYYY-MM-DD`. + +```json +{ + "date": "2026-06-10", + "total": {"total": 5570, "minutes": 40.2}, + "cameras": [ + { + "camera_id": "CC1", + "total_entered": 1500, + "frames_processed": 24800, + "elapsed_seconds": 600.5, + "stopped_reason": "backward", + "source_video": "kandang_1_camera_1_2026-06-10_120056.mp4", + "location": "kandang-atas" + } + ] +} +``` + +### `GET /api/db/camera/` +History for a specific camera across all dates (max 50 rows). + +```json +[ + { + "date": "2026-06-10", + "location": "kandang-atas", + "total_entered": 1500, + "frames_processed": 24800, + "elapsed_seconds": 600.5, + "stopped_reason": "backward" + } +] +``` + +### `GET /api/db/location/` +Summary and history for a specific location. + +```json +{ + "location": "kandang-atas", + "summary": {"days": 5, "total": 25000, "hours": 3.2}, + "history": [ + { + "date": "2026-06-10", + "cameras": "CC1, CC2, CC3, CC4", + "total": 5570, + "minutes": 40.2 + } + ] +} +``` + +--- + +## Database Schema + +```sql +CREATE TABLE 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) +); +``` + +Data is inserted automatically by the batch runner when `location` and `db_path` are configured in the batch YAML, or manually via: + +```bash +python3 store_results.py output/counts_2026-06-10.json --location kandang-atas --db chicken_counts.db +``` diff --git a/dashboard.py b/dashboard.py index d849f71..d49c0e6 100755 --- a/dashboard.py +++ b/dashboard.py @@ -66,13 +66,37 @@ def shm_frame(camera_id): return send_file(frame_path, mimetype="image/jpeg", max_age=0, download_name=None) -@app.route("/api/db/history") -def db_history(): +def _get_db(): db = app.config["db_path"] if not db or not Path(db).exists(): - return jsonify([]) - + return None conn = sqlite3.connect(db) + conn.row_factory = sqlite3.Row + return conn + + +@app.route("/api/db/summary") +def db_summary(): + conn = _get_db() + if not conn: + return jsonify({}) + row = conn.execute(""" + SELECT COUNT(DISTINCT date) AS days, + COUNT(DISTINCT location) AS locations, + COUNT(*) AS total_runs, + SUM(total_entered) AS total_chickens, + ROUND(SUM(elapsed_seconds)/3600.0, 1) AS total_hours + FROM batch_runs + """).fetchone() + conn.close() + return jsonify(dict(row)) + + +@app.route("/api/db/history") +def db_history(): + conn = _get_db() + if not conn: + return jsonify([]) rows = conn.execute(""" SELECT date, location, COUNT(*) AS cams, @@ -84,12 +108,59 @@ def db_history(): LIMIT 50 """).fetchall() conn.close() + return jsonify([dict(r) for r in rows]) - return jsonify([ - {"date": r[0], "location": r[1], "cams": r[2], - "total": r[3], "minutes": r[4]} - for r in rows - ]) + +@app.route("/api/db/date/") +def db_date(date): + conn = _get_db() + if not conn: + return jsonify({}) + cameras = conn.execute(""" + SELECT camera_id, total_entered, frames_processed, + ROUND(elapsed_seconds,1) AS elapsed_seconds, + stopped_reason, source_video, location + FROM batch_runs WHERE date=? ORDER BY camera_id + """, (date,)).fetchall() + total = conn.execute( + "SELECT SUM(total_entered) AS total, ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes " + "FROM batch_runs WHERE date=?", (date,)).fetchone() + conn.close() + return jsonify({"date": date, "total": dict(total), "cameras": [dict(r) for r in cameras]}) + + +@app.route("/api/db/camera/") +def db_camera(camera_id): + conn = _get_db() + if not conn: + return jsonify([]) + rows = conn.execute(""" + SELECT date, location, total_entered, frames_processed, + ROUND(elapsed_seconds,1) AS elapsed_seconds, stopped_reason + FROM batch_runs WHERE camera_id=? ORDER BY date DESC LIMIT 50 + """, (camera_id,)).fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) + + +@app.route("/api/db/location/") +def db_location(location): + conn = _get_db() + if not conn: + return jsonify({}) + history = conn.execute(""" + SELECT date, GROUP_CONCAT(camera_id,', ') AS cameras, + SUM(total_entered) AS total, + ROUND(SUM(elapsed_seconds)/60.0,1) AS minutes + FROM batch_runs WHERE location=? GROUP BY date ORDER BY date DESC LIMIT 50 + """, (location,)).fetchall() + summary = conn.execute(""" + SELECT COUNT(DISTINCT date) AS days, SUM(total_entered) AS total, + ROUND(SUM(elapsed_seconds)/3600.0,1) AS hours + FROM batch_runs WHERE location=? + """, (location,)).fetchone() + conn.close() + return jsonify({"location": location, "summary": dict(summary), "history": [dict(r) for r in history]}) def main():