Zenai Karung Pakan Counter

This commit is contained in:
proitlab committed 2026-07-09 23:46:18 +07:00
commit 3b8fb0c312
8 files changed
+3674

No files matched your search

+390
View File
@@ -0,0 +1,390 @@
#!/usr/bin/env python3
"""
Edge Jetson production counter dashboard.
Reads jetson_counter.db + current_counter.json from the counter stack.
Tracks daily counter_in / counter_out and total per counting day (no batches).
Default port 5000.
"""
import json
import os
import sqlite3
import time
from io import BytesIO
from datetime import datetime, timedelta
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment, PatternFill, Border, Side
from flask import Flask, render_template, jsonify, request, Response
from werkzeug.serving import WSGIRequestHandler
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__, template_folder="templates")
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "change-me-in-production")
_DEFAULT_DIR = "/opt/jetson-counter"
DB_PATH = os.getenv("DB_PATH", f"{_DEFAULT_DIR}/jetson_counter.db")
CURRENT_COUNTER_PATH = os.getenv("STATE_FILE", os.getenv("CURRENT_COUNTER_PATH", f"{_DEFAULT_DIR}/current_counter.json"))
CUTOFF_TIME = os.getenv("CUTOFF_TIME", os.getenv("DAILY_CUTOFF_TIME", "20:00"))
LIVE_STREAM_FRAME_PATH = os.getenv("LIVE_STREAM_FRAME_PATH", "/dev/shm/jetson-counter/live_frame.jpg")
SITE_NAME = os.getenv("SITE_NAME", "LIVE")
DASHBOARD_PORT = int(os.getenv("DASHBOARD_PORT", "5000"))
DASHBOARD_HOST = os.getenv("DASHBOARD_HOST", "0.0.0.0")
FLASK_DEBUG = os.getenv("FLASK_DEBUG", "false").lower() == "true"
@app.route("/api/live-video")
def api_live_video():
if not os.path.isfile(LIVE_STREAM_FRAME_PATH):
return jsonify({"success": False, "error": "Live stream frame not available yet"}), 503
def generate():
consecutive_fails = 0
MAX_FAILS = 30
while True:
try:
with open(LIVE_STREAM_FRAME_PATH, "rb") as f:
jpeg = f.read()
consecutive_fails = 0
yield (b"--frame\r\n"
b"Content-Type: image/jpeg\r\n\r\n" + jpeg + b"\r\n")
except FileNotFoundError:
consecutive_fails += 1
if consecutive_fails >= MAX_FAILS:
return
time.sleep(1.0)
continue
except Exception:
consecutive_fails += 1
if consecutive_fails >= MAX_FAILS:
return
time.sleep(0.5)
continue
time.sleep(0.05)
return Response(generate(), mimetype="multipart/x-mixed-replace; boundary=frame")
def _ensure_db():
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute(
"""
CREATE TABLE IF NOT EXISTS daily_counters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
counting_date TEXT NOT NULL,
camera_name TEXT NOT NULL,
object_label TEXT NOT NULL,
total_count INTEGER NOT NULL DEFAULT 0,
total_in INTEGER NOT NULL DEFAULT 0,
total_out INTEGER NOT NULL DEFAULT 0,
start_time TEXT,
end_time TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(counting_date, camera_name, object_label)
)
"""
)
conn.commit()
conn.close()
_ensure_db()
def get_db():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
def get_counting_date(dt=None, cutoff_str=CUTOFF_TIME):
if dt is None:
dt = datetime.now()
cutoff = datetime.strptime(cutoff_str, "%H:%M").time()
if dt.time() < cutoff:
return dt.date().isoformat()
return (dt.date() + timedelta(days=1)).isoformat()
@app.route("/")
def index():
return render_template("dashboard.html", site_name=SITE_NAME)
@app.route("/api/current-counter")
def api_current_counter():
try:
with open(CURRENT_COUNTER_PATH, "r") as f:
data = json.load(f)
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"),
}
)
except FileNotFoundError:
return jsonify(
{
"success": False,
"error": "No active counter",
"count": 0,
"count_in": 0,
"count_out": 0,
"counting_date": None,
}
), 200
except Exception as e:
return jsonify(
{
"success": False,
"error": str(e),
"count": 0,
"count_in": 0,
"count_out": 0,
"counting_date": None,
}
), 500
@app.route("/api/summary")
def api_summary():
try:
conn = get_db()
cur = conn.cursor()
today = get_counting_date()
cur.execute(
"""
SELECT COALESCE(total_count, 0) as total_count,
COALESCE(total_in, 0) as total_in,
COALESCE(total_out, 0) as total_out
FROM daily_counters
WHERE counting_date = ?
""",
(today,),
)
today_row = cur.fetchone()
yesterday = (datetime.strptime(today, "%Y-%m-%d").date() - timedelta(days=1)).isoformat()
cur.execute(
"""
SELECT COALESCE(total_count, 0) as total_count,
COALESCE(total_in, 0) as total_in,
COALESCE(total_out, 0) as total_out
FROM daily_counters
WHERE counting_date = ?
""",
(yesterday,),
)
yesterday_row = cur.fetchone()
cur.execute(
"""
SELECT COALESCE(SUM(total_count), 0) as grand_total,
COALESCE(SUM(total_in), 0) as grand_in,
COALESCE(SUM(total_out), 0) as grand_out,
COUNT(DISTINCT counting_date) as total_days
FROM daily_counters
"""
)
all_time = cur.fetchone()
cur.execute("SELECT ROUND(AVG(total_count), 1) as avg_per_day FROM daily_counters")
avg = cur.fetchone()
cur.execute(
"""
SELECT counting_date, total_count
FROM daily_counters
ORDER BY total_count DESC
LIMIT 1
"""
)
best = cur.fetchone()
conn.close()
return jsonify(
{
"today": {
"date": today,
"total_count": today_row["total_count"] if today_row else 0,
"total_in": today_row["total_in"] if today_row else 0,
"total_out": today_row["total_out"] if today_row else 0,
},
"yesterday": {
"date": yesterday,
"total_count": yesterday_row["total_count"] if yesterday_row else 0,
"total_in": yesterday_row["total_in"] if yesterday_row else 0,
"total_out": yesterday_row["total_out"] if yesterday_row else 0,
},
"all_time": {
"grand_total": all_time["grand_total"],
"grand_in": all_time["grand_in"],
"grand_out": all_time["grand_out"],
"total_days": all_time["total_days"],
},
"average_per_day": avg["avg_per_day"] or 0,
"best_day": {
"date": best["counting_date"] if best else None,
"count": best["total_count"] if best else 0,
},
}
)
except sqlite3.OperationalError as e:
return jsonify({"success": False, "error": f"Database unavailable: {e}", "today": {"date": datetime.now().date().isoformat(), "total_count": 0, "total_in": 0, "total_out": 0}, "yesterday": {"date": "", "total_count": 0, "total_in": 0, "total_out": 0}, "all_time": {"grand_total": 0, "grand_in": 0, "grand_out": 0, "total_days": 0}, "average_per_day": 0, "best_day": {"date": None, "count": 0}}), 200
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route("/api/daily-data")
def api_daily_data():
try:
days = request.args.get("days", 30, type=int)
date_from = (datetime.now() - timedelta(days=days)).date().isoformat()
conn = get_db()
cur = conn.cursor()
cur.execute(
"""
SELECT counting_date, total_count, total_in, total_out
FROM daily_counters
WHERE counting_date >= ?
ORDER BY counting_date ASC
""",
(date_from,),
)
daily_data = [
{
"date": row["counting_date"],
"total_count": row["total_count"],
"total_in": row["total_in"],
"total_out": row["total_out"],
}
for row in cur.fetchall()
]
conn.close()
return jsonify(daily_data)
except sqlite3.OperationalError:
return jsonify([]), 200
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route("/api/available-dates")
def api_available_dates():
try:
conn = get_db()
cur = conn.cursor()
cur.execute(
"""
SELECT counting_date, total_count, total_in, total_out, start_time, end_time
FROM daily_counters
ORDER BY counting_date DESC
"""
)
dates = [
{
"date": row["counting_date"],
"total_count": row["total_count"],
"total_in": row["total_in"],
"total_out": row["total_out"],
"start_time": row["start_time"],
"end_time": row["end_time"],
}
for row in cur.fetchall()
]
conn.close()
return jsonify(dates)
except sqlite3.OperationalError:
return jsonify([]), 200
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
def _excel_response(wb, filename):
output = BytesIO()
wb.save(output)
output.seek(0)
return Response(
output.getvalue(),
mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={filename}"},
)
def _style_header(ws, cols):
header_font = Font(bold=True, color="FFFFFF", size=11)
header_fill = PatternFill(start_color="2F5496", end_color="2F5496", fill_type="solid")
thin_border = Border(
left=Side(style="thin"), right=Side(style="thin"),
top=Side(style="thin"), bottom=Side(style="thin"),
)
for col_idx, (col_letter, text) in enumerate(cols, 1):
cell = ws.cell(row=1, column=col_idx, value=text)
cell.font = header_font
cell.fill = header_fill
cell.alignment = Alignment(horizontal="center")
cell.border = thin_border
ws.freeze_panes = "A2"
def _auto_width(ws):
for col in ws.columns:
max_len = 0
col_letter = col[0].column_letter
for cell in col:
if cell.value is not None:
max_len = max(max_len, len(str(cell.value)))
ws.column_dimensions[col_letter].width = max_len + 4
@app.route("/api/export-daily-csv")
def export_daily_xlsx():
try:
days = request.args.get("days", 30, type=int)
date_from = (datetime.now() - timedelta(days=days)).date().isoformat()
conn = get_db()
cur = conn.cursor()
cur.execute(
"""
SELECT counting_date, total_count, total_in, total_out, start_time, end_time
FROM daily_counters
WHERE counting_date >= ?
ORDER BY counting_date ASC
""",
(date_from,),
)
rows = cur.fetchall()
conn.close()
except sqlite3.OperationalError as e:
return jsonify({"success": False, "error": f"Database unavailable: {e}"}), 503
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
wb = Workbook()
ws = wb.active
ws.title = "Daily Counters"
_style_header(ws, [("A", "Date"), ("B", "Total"), ("C", "In"), ("D", "Out"), ("E", "First Count"), ("F", "Last Count")])
for r_idx, row in enumerate(rows, 2):
ws.cell(row=r_idx, column=1, value=row["counting_date"])
ws.cell(row=r_idx, column=2, value=row["total_count"])
ws.cell(row=r_idx, column=3, value=row["total_in"])
ws.cell(row=r_idx, column=4, value=row["total_out"])
ws.cell(row=r_idx, column=5, value=row["start_time"])
ws.cell(row=r_idx, column=6, value=row["end_time"])
_auto_width(ws)
filename = f"{SITE_NAME}_daily_records_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return _excel_response(wb, filename)
if __name__ == "__main__":
WSGIRequestHandler.protocol_version = "HTTP/1.1"
print(f"Jetson counter dashboard at http://{DASHBOARD_HOST}:{DASHBOARD_PORT}")
print(f"DB: {DB_PATH}")
print(f"State: {CURRENT_COUNTER_PATH}")
app.run(host=DASHBOARD_HOST, port=DASHBOARD_PORT, debug=FLASK_DEBUG)