Add DB API endpoints: summary, history, date, camera, location
- api/db/summary — overall totals - api/db/history — per-date rows - api/db/date/<date> — single date detail with per-camera breakdown - api/db/camera/<id> — all runs for a specific camera - api/db/location/<name> — per-location summary + history - API.md — full endpoint documentation with schema
This commit is contained in:
1 parent
efd4726bc6
commit
ae86821165
2 files changed
+229
-9
No files matched your search
@@ -0,0 +1,149 @@
|
|||||||
|
# Chicken Counter API
|
||||||
|
|
||||||
|
Base URL: `http://<jetson-ip>: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/<camera_id>/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/<camera_id>/frame.jpg`
|
||||||
|
Live JPEG frame from the active pipeline.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
All endpoints require the dashboard to be started with `--db <path>`. 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/<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/<camera_id>`
|
||||||
|
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/<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
|
||||||
|
```
|
||||||
+80
-9
@@ -66,13 +66,37 @@ def shm_frame(camera_id):
|
|||||||
return send_file(frame_path, mimetype="image/jpeg", max_age=0, download_name=None)
|
return send_file(frame_path, mimetype="image/jpeg", max_age=0, download_name=None)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/db/history")
|
def _get_db():
|
||||||
def db_history():
|
|
||||||
db = app.config["db_path"]
|
db = app.config["db_path"]
|
||||||
if not db or not Path(db).exists():
|
if not db or not Path(db).exists():
|
||||||
return jsonify([])
|
return None
|
||||||
|
|
||||||
conn = sqlite3.connect(db)
|
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("""
|
rows = conn.execute("""
|
||||||
SELECT date, location,
|
SELECT date, location,
|
||||||
COUNT(*) AS cams,
|
COUNT(*) AS cams,
|
||||||
@@ -84,12 +108,59 @@ def db_history():
|
|||||||
LIMIT 50
|
LIMIT 50
|
||||||
""").fetchall()
|
""").fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
return jsonify([dict(r) for r in rows])
|
||||||
|
|
||||||
return jsonify([
|
|
||||||
{"date": r[0], "location": r[1], "cams": r[2],
|
@app.route("/api/db/date/<date>")
|
||||||
"total": r[3], "minutes": r[4]}
|
def db_date(date):
|
||||||
for r in rows
|
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/<camera_id>")
|
||||||
|
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/<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():
|
def main():
|
||||||
|
|||||||
Reference in new issue
Block a user