forked from dsutanto/zenai-kpc-python
update API
This commit is contained in:
1 parent
783b785b4f
commit
2b52ce6e6d
1 file changed
+249
-20
+249
-20
@@ -266,46 +266,275 @@ def snapshots_page():
|
||||
return render_template("snapshots.html", site_name=SITE_NAME, show_detect=SAVE_DETECT_SNAPSHOT)
|
||||
|
||||
|
||||
def _empty_current():
|
||||
return {
|
||||
"counting_date": get_counting_date(),
|
||||
"count": 0,
|
||||
"count_in": 0,
|
||||
"count_out": 0,
|
||||
"start_time": None,
|
||||
"last_detection_time": None,
|
||||
}
|
||||
|
||||
|
||||
def _load_current_state():
|
||||
"""Load live counter state from current_counter.json."""
|
||||
with open(CURRENT_COUNTER_PATH, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
return {
|
||||
"counting_date": data.get("counting_date") or get_counting_date(),
|
||||
"count": int(data.get("count", 0) or 0),
|
||||
"count_in": int(data.get("count_in", 0) or 0),
|
||||
"count_out": int(data.get("count_out", 0) or 0),
|
||||
"start_time": data.get("start_time"),
|
||||
"last_detection_time": data.get("last_detection_time"),
|
||||
}
|
||||
|
||||
|
||||
def _query_history(date_from=None, date_to=None, days=None, limit=None, offset=0):
|
||||
"""Query daily_counters with optional date range and pagination."""
|
||||
conn = get_db()
|
||||
cur = conn.cursor()
|
||||
|
||||
clauses = []
|
||||
params = []
|
||||
|
||||
if days is not None and date_from is None and date_to is None:
|
||||
date_from = (datetime.now() - timedelta(days=days)).date().isoformat()
|
||||
|
||||
if date_from:
|
||||
clauses.append("counting_date >= ?")
|
||||
params.append(date_from)
|
||||
if date_to:
|
||||
clauses.append("counting_date <= ?")
|
||||
params.append(date_to)
|
||||
|
||||
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
|
||||
|
||||
cur.execute(f"SELECT COUNT(*) AS total FROM daily_counters {where}", params)
|
||||
total = cur.fetchone()["total"]
|
||||
|
||||
sql = f"""
|
||||
SELECT counting_date, camera_name, object_label,
|
||||
total_count, total_in, total_out, start_time, end_time, updated_at
|
||||
FROM daily_counters
|
||||
{where}
|
||||
ORDER BY counting_date DESC
|
||||
"""
|
||||
page_params = list(params)
|
||||
if limit is not None:
|
||||
sql += " LIMIT ? OFFSET ?"
|
||||
page_params.extend([limit, offset])
|
||||
|
||||
cur.execute(sql, page_params)
|
||||
rows = [
|
||||
{
|
||||
"date": row["counting_date"],
|
||||
"camera_name": row["camera_name"],
|
||||
"object_label": row["object_label"],
|
||||
"total_count": row["total_count"],
|
||||
"total_in": row["total_in"],
|
||||
"total_out": row["total_out"],
|
||||
"diff": (row["total_in"] or 0) - (row["total_out"] or 0),
|
||||
"start_time": row["start_time"],
|
||||
"end_time": row["end_time"],
|
||||
"updated_at": row["updated_at"],
|
||||
}
|
||||
for row in cur.fetchall()
|
||||
]
|
||||
conn.close()
|
||||
return rows, total
|
||||
|
||||
|
||||
@app.route("/api/current")
|
||||
@app.route("/api/current-counter")
|
||||
def api_current_counter():
|
||||
def api_current():
|
||||
"""Current counting-day totals from live state file."""
|
||||
try:
|
||||
with open(CURRENT_COUNTER_PATH, "r") as f:
|
||||
data = json.load(f)
|
||||
state = _load_current_state()
|
||||
return jsonify(
|
||||
{
|
||||
"success": True,
|
||||
"counting_date": data.get("counting_date"),
|
||||
"count": data.get("count", 0),
|
||||
"count_in": data.get("count_in", 0),
|
||||
"count_out": data.get("count_out", 0),
|
||||
"start_time": data.get("start_time"),
|
||||
"last_detection_time": data.get("last_detection_time"),
|
||||
"active": True,
|
||||
"site_name": SITE_NAME,
|
||||
"counting": _read_counting_flag() if CONTROL_ENABLED else True,
|
||||
**state,
|
||||
}
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"error": "No active counter",
|
||||
"count": 0,
|
||||
"count_in": 0,
|
||||
"count_out": 0,
|
||||
"counting_date": None,
|
||||
"success": True,
|
||||
"site_name": SITE_NAME,
|
||||
"counting": _read_counting_flag() if CONTROL_ENABLED else True,
|
||||
"active": False,
|
||||
"error": "No active counter — state file missing",
|
||||
**_empty_current(),
|
||||
}
|
||||
), 200
|
||||
except Exception as e:
|
||||
return jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"count": 0,
|
||||
"count_in": 0,
|
||||
"count_out": 0,
|
||||
"counting_date": None,
|
||||
"error": f"Failed to read current counter from {CURRENT_COUNTER_PATH}: {e}",
|
||||
"site_name": SITE_NAME,
|
||||
**_empty_current(),
|
||||
}
|
||||
), 500
|
||||
|
||||
|
||||
@app.route("/api/history")
|
||||
def api_history():
|
||||
"""
|
||||
Historical daily counters.
|
||||
|
||||
Query params:
|
||||
days – last N calendar days (default 30; ignored if date_from/date_to set)
|
||||
date_from – inclusive YYYY-MM-DD
|
||||
date_to – inclusive YYYY-MM-DD
|
||||
limit – page size (default: all matching)
|
||||
offset – page offset (default 0)
|
||||
"""
|
||||
try:
|
||||
days = request.args.get("days", type=int)
|
||||
date_from = request.args.get("date_from")
|
||||
date_to = request.args.get("date_to")
|
||||
limit = request.args.get("limit", type=int)
|
||||
offset = request.args.get("offset", 0, type=int)
|
||||
|
||||
if date_from:
|
||||
try:
|
||||
datetime.strptime(date_from, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": f"Invalid date_from '{date_from}'. Use YYYY-MM-DD.",
|
||||
},
|
||||
}), 400
|
||||
if date_to:
|
||||
try:
|
||||
datetime.strptime(date_to, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": f"Invalid date_to '{date_to}'. Use YYYY-MM-DD.",
|
||||
},
|
||||
}), 400
|
||||
|
||||
if days is None and date_from is None and date_to is None:
|
||||
days = 30
|
||||
if offset < 0:
|
||||
offset = 0
|
||||
if limit is not None and limit < 1:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "limit must be a positive integer",
|
||||
},
|
||||
}), 400
|
||||
|
||||
rows, total = _query_history(
|
||||
date_from=date_from,
|
||||
date_to=date_to,
|
||||
days=days,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
payload = {
|
||||
"success": True,
|
||||
"site_name": SITE_NAME,
|
||||
"filters": {
|
||||
"days": days,
|
||||
"date_from": date_from,
|
||||
"date_to": date_to,
|
||||
},
|
||||
"count": len(rows),
|
||||
"data": rows,
|
||||
}
|
||||
if limit is not None:
|
||||
payload["pagination"] = {
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"total": total,
|
||||
"has_next": offset + limit < total,
|
||||
"has_prev": offset > 0,
|
||||
}
|
||||
else:
|
||||
payload["total"] = total
|
||||
return jsonify(payload)
|
||||
except sqlite3.OperationalError as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "DATABASE_UNAVAILABLE",
|
||||
"message": f"Database unavailable at {DB_PATH}: {e}",
|
||||
},
|
||||
"data": [],
|
||||
"count": 0,
|
||||
"total": 0,
|
||||
}), 200
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": str(e),
|
||||
},
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route("/api/history/<counting_date>")
|
||||
def api_history_day(counting_date):
|
||||
"""Single counting-day record by YYYY-MM-DD."""
|
||||
try:
|
||||
datetime.strptime(counting_date, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": f"Invalid counting_date '{counting_date}'. Use YYYY-MM-DD.",
|
||||
},
|
||||
}), 400
|
||||
|
||||
try:
|
||||
rows, _ = _query_history(date_from=counting_date, date_to=counting_date)
|
||||
if not rows:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "NOT_FOUND",
|
||||
"message": f"No history found for counting date {counting_date}",
|
||||
},
|
||||
}), 404
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"site_name": SITE_NAME,
|
||||
"data": rows[0] if len(rows) == 1 else rows,
|
||||
})
|
||||
except sqlite3.OperationalError as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "DATABASE_UNAVAILABLE",
|
||||
"message": f"Database unavailable at {DB_PATH}: {e}",
|
||||
},
|
||||
}), 503
|
||||
except Exception as e:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": {
|
||||
"code": "INTERNAL_ERROR",
|
||||
"message": str(e),
|
||||
},
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route("/api/summary")
|
||||
def api_summary():
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user