From 078c91283dbdbfb8d0b1a7500a923c0003c585c8 Mon Sep 17 00:00:00 2001 From: proitlab Date: Tue, 3 Mar 2026 11:27:29 +0700 Subject: [PATCH] Web Apps --- requirements.txt | 3 +- run_web.sh | 29 +++++ templates/base.html | 222 +++++++++++++++++++++++++++++++++++ templates/camera_detail.html | 52 ++++++++ templates/date_detail.html | 58 +++++++++ templates/index.html | 123 +++++++++++++++++++ web_app.py | 221 ++++++++++++++++++++++++++++++++++ 7 files changed, 707 insertions(+), 1 deletion(-) create mode 100755 run_web.sh create mode 100644 templates/base.html create mode 100644 templates/camera_detail.html create mode 100644 templates/date_detail.html create mode 100644 templates/index.html create mode 100644 web_app.py diff --git a/requirements.txt b/requirements.txt index 47b2242..60327ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ paho-mqtt>=1.6.1 -schedule>=1.2.0 \ No newline at end of file +schedule>=1.2.0 +flask>=2.0.0 \ No newline at end of file diff --git a/run_web.sh b/run_web.sh new file mode 100755 index 0000000..293c79e --- /dev/null +++ b/run_web.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Start the Flask web application + +cd "$(dirname "$0")" + +# Check if virtual environment exists, if not create it +if [ ! -d "venv" ]; then + echo "Creating virtual environment..." + python3 -m venv venv +fi + +# Activate virtual environment +source venv/bin/activate + +# Install dependencies +echo "Installing dependencies..." +pip install -q -r requirements.txt + +# Check if database exists +if [ ! -f "karung_counts.db" ]; then + echo "Error: Database karung_counts.db not found!" + echo "Please run the frigate_counter.py first to initialize the database." + exit 1 +fi + +# Run the Flask application +echo "Starting Flask web application..." +echo "Access the dashboard at: http://localhost:5000" +python web_app.py diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..09f75cb --- /dev/null +++ b/templates/base.html @@ -0,0 +1,222 @@ + + + + + + {% block title %}Frigate Counter Dashboard{% endblock %} + + {% block extra_css %}{% endblock %} + + +
+
+

Frigate Counter Dashboard

+ +
+
+ +
+ {% block content %}{% endblock %} +
+ + + + {% block extra_js %}{% endblock %} + + diff --git a/templates/camera_detail.html b/templates/camera_detail.html new file mode 100644 index 0000000..76fecc1 --- /dev/null +++ b/templates/camera_detail.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block title %}{{ camera_name }} - Camera Details{% endblock %} + +{% block content %} +
+

Camera: {{ camera_name }}

+ +
+
+

{{ counts|length }}

+

Days with Data

+
+
+

{{ total }}

+

Total Counts

+
+
+

{{ (total / counts|length)|round(1) if counts|length > 0 else 0 }}

+

Average per Day

+
+
+ + {% if counts %} + + + + + + + + + + {% for item in counts %} + + + + + + {% endfor %} + +
DateCountTimestamp
{{ item.date }}{{ item.counter_value }}{{ item.timestamp }}
+ {% else %} +
+

No data for this camera

+

There are no records for camera {{ camera_name }}.

+
+ {% endif %} + + ← Back to Dashboard +
+{% endblock %} diff --git a/templates/date_detail.html b/templates/date_detail.html new file mode 100644 index 0000000..5809e93 --- /dev/null +++ b/templates/date_detail.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}{{ date }} - Daily Details{% endblock %} + +{% block content %} +
+

Details for {{ date }}

+ +
+ + +
+ +
+
+

{{ counts|length }}

+

Camera Entries

+
+
+

{{ total }}

+

Total Counts

+
+
+ + {% if counts %} + + + + + + + + + + {% for item in counts %} + + + + + + {% endfor %} + +
Camera NameCountTimestamp
{{ item.camera_name }}{{ item.counter_value }}{{ item.timestamp }}
+ {% else %} +
+

No data for this date

+

There are no records for {{ date }}.

+
+ {% endif %} + + ← Back to Dashboard +
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..07977c2 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,123 @@ +{% extends "base.html" %} + +{% block title %}Dashboard - Frigate Counter{% endblock %} + +{% block content %} + +
+
+

{{ today_data|length }}

+

Cameras Active Today

+
+
+

{{ today_data|sum(attribute='counter_value') }}

+

Total Counts Today

+
+
+

{{ summary|length }}

+

Days with Data

+
+
+

{{ cameras|sum(attribute='total_count') }}

+

All Time Total

+
+
+ + +
+

Today's Activity ({{ today }})

+ {% if today_data %} + + + + + + + + + + {% for item in today_data %} + + + + + + {% endfor %} + +
CameraCountLast Updated
{{ item.camera_name }}{{ item.counter_value }}{{ item.timestamp }}
+ {% else %} +
+

No data for today yet

+

Counts will appear here once the counter starts receiving events.

+
+ {% endif %} +
+ + +
+

Daily Summary

+ {% if summary %} + + + + + + + + + + + + {% for day in summary %} + + + + + + + + {% endfor %} + +
DateCamerasTotal CountsLast UpdateAction
{{ day.date }}{{ day.camera_count }}{{ day.total_count }}{{ day.last_update }}View Details
+ {% else %} +
+

No data available

+

Daily summaries will appear here once data is recorded.

+
+ {% endif %} +
+ + +
+

Camera Summary

+ {% if cameras %} + + + + + + + + + + + + {% for camera in cameras %} + + + + + + + + {% endfor %} + +
Camera NameDays ActiveTotal CountsLast UpdateAction
{{ camera.camera_name }}{{ camera.day_count }}{{ camera.total_count }}{{ camera.last_update }}View Details
+ {% else %} +
+

No cameras registered

+

Camera data will appear here once events are processed.

+
+ {% endif %} +
+{% endblock %} diff --git a/web_app.py b/web_app.py new file mode 100644 index 0000000..34160ea --- /dev/null +++ b/web_app.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +""" +Flask Web Application for Frigate Counter +Displays daily karung counts from SQLite database +""" + +from flask import Flask, render_template, jsonify, request +import sqlite3 +from datetime import datetime, date, timedelta +from typing import List, Dict, Optional +import os + +app = Flask(__name__) +DB_PATH = 'karung_counts.db' + + +def get_db_connection(): + """Create a database connection""" + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + + +def get_daily_counts(selected_date: Optional[str] = None) -> List[Dict]: + """Get counts for a specific date or all dates""" + conn = get_db_connection() + cursor = conn.cursor() + + if selected_date: + cursor.execute(''' + SELECT camera_name, date, counter_value, timestamp + FROM karung_counts + WHERE date = ? + ORDER BY timestamp DESC + ''', (selected_date,)) + else: + cursor.execute(''' + SELECT camera_name, date, counter_value, timestamp + FROM karung_counts + ORDER BY date DESC, timestamp DESC + ''') + + rows = cursor.fetchall() + conn.close() + + return [{ + 'camera_name': row['camera_name'], + 'date': row['date'], + 'counter_value': row['counter_value'], + 'timestamp': row['timestamp'] + } for row in rows] + + +def get_summary_by_date() -> List[Dict]: + """Get daily summary grouped by date""" + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT + date, + COUNT(DISTINCT camera_name) as camera_count, + SUM(counter_value) as total_count, + MAX(timestamp) as last_update + FROM karung_counts + GROUP BY date + ORDER BY date DESC + ''') + + rows = cursor.fetchall() + conn.close() + + return [{ + 'date': row['date'], + 'camera_count': row['camera_count'], + 'total_count': row['total_count'] or 0, + 'last_update': row['last_update'] + } for row in rows] + + +def get_available_dates() -> List[str]: + """Get list of all dates with data""" + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT DISTINCT date + FROM karung_counts + ORDER BY date DESC + ''') + + rows = cursor.fetchall() + conn.close() + + return [row['date'] for row in rows] + + +def get_camera_summary() -> List[Dict]: + """Get summary by camera""" + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT + camera_name, + COUNT(DISTINCT date) as day_count, + SUM(counter_value) as total_count, + MAX(timestamp) as last_update + FROM karung_counts + GROUP BY camera_name + ORDER BY camera_name + ''') + + rows = cursor.fetchall() + conn.close() + + return [{ + 'camera_name': row['camera_name'], + 'day_count': row['day_count'], + 'total_count': row['total_count'] or 0, + 'last_update': row['last_update'] + } for row in rows] + + +@app.route('/') +def index(): + """Main page showing daily summary""" + summary = get_summary_by_date() + cameras = get_camera_summary() + available_dates = get_available_dates() + + today = date.today().isoformat() + today_data = get_daily_counts(today) + + return render_template('index.html', + summary=summary, + cameras=cameras, + today_data=today_data, + available_dates=available_dates, + today=today) + + +@app.route('/date/') +def date_detail(date_str: str): + """Show details for a specific date""" + counts = get_daily_counts(date_str) + available_dates = get_available_dates() + + # Calculate total for this date + total = sum(c['counter_value'] for c in counts) + + return render_template('date_detail.html', + date=date_str, + counts=counts, + total=total, + available_dates=available_dates) + + +@app.route('/camera/') +def camera_detail(camera_name: str): + """Show details for a specific camera""" + conn = get_db_connection() + cursor = conn.cursor() + + cursor.execute(''' + SELECT date, counter_value, timestamp + FROM karung_counts + WHERE camera_name = ? + ORDER BY date DESC + ''', (camera_name,)) + + rows = cursor.fetchall() + conn.close() + + counts = [{ + 'date': row['date'], + 'counter_value': row['counter_value'], + 'timestamp': row['timestamp'] + } for row in rows] + + total = sum(c['counter_value'] for c in counts) + + return render_template('camera_detail.html', + camera_name=camera_name, + counts=counts, + total=total) + + +@app.route('/api/counts') +def api_counts(): + """API endpoint to get all counts""" + date_filter = request.args.get('date') + counts = get_daily_counts(date_filter) + return jsonify({ + 'counts': counts, + 'date_filter': date_filter + }) + + +@app.route('/api/summary') +def api_summary(): + """API endpoint to get daily summary""" + summary = get_summary_by_date() + return jsonify({'summary': summary}) + + +@app.route('/api/cameras') +def api_cameras(): + """API endpoint to get camera summary""" + cameras = get_camera_summary() + return jsonify({'cameras': cameras}) + + +if __name__ == '__main__': + # Ensure database exists + if not os.path.exists(DB_PATH): + print(f"Error: Database {DB_PATH} not found!") + exit(1) + + # Run Flask app on all interfaces, port 5000 + app.run(host='0.0.0.0', port=5000, debug=True)