From 078c91283dbdbfb8d0b1a7500a923c0003c585c8 Mon Sep 17 00:00:00 2001 From: proitlab Date: Tue, 3 Mar 2026 11:27:29 +0700 Subject: [PATCH 1/7] 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) From 14005331ac8268d24ec8b34be45df79b025d5f1c Mon Sep 17 00:00:00 2001 From: dsutanto Date: Thu, 5 Mar 2026 15:07:36 +0700 Subject: [PATCH 2/7] Ensure pintu-tutup in pintu_tutup zone --- frigate_counter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frigate_counter.py b/frigate_counter.py index 3a21f25..e6702f8 100644 --- a/frigate_counter.py +++ b/frigate_counter.py @@ -28,12 +28,13 @@ class FrigateCounter: # MQTT configuration from environment variables self.frigate_mqtt_host = os.environ.get('FRIGATE_MQTT_HOST', 'localhost') self.frigate_mqtt_port = int(os.environ.get('FRIGATE_MQTT_PORT', 1883)) - self.report_mqtt_host = os.environ.get('REPORT_MQTT_HOST', 'localhost') + self.report_mqtt_host = os.environ.get('REPORT_MQTT_HOST', 'mqtt.backone.cloud') self.report_mqtt_port = int(os.environ.get('REPORT_MQTT_PORT', 1883)) self.top_topic = os.environ.get("TOP_TOPIC", "cpsp") self.site_name = os.environ.get('SITE_NAME', 'sukawarna') self.topic = os.environ.get('TOPIC', f"{self.top_topic}/counter/{self.site_name}") self.camera_name = os.environ.get('CAMERA_NAME', 'kandang_1_karung_masuk') + self.pintu_tutup_zone_name = os.environ.get('PINTU_TUTUP_ZONE_NAME', 'pintu_tutup') logger.info(f"FRIGATE_MQTT_HOST: {self.frigate_mqtt_host}:{self.frigate_mqtt_port}") logger.info(f"REPORT_MQTT_HOST: {self.report_mqtt_host}:{self.report_mqtt_port}") @@ -179,7 +180,7 @@ class FrigateCounter: self.handle_pintu_kanan_buka(camera_name) elif label == "karung" and self.timer_active: self.handle_karung(camera_name, track_id) - elif label == "pintu-tutup" and self.timer_active: + elif label == "pintu-tutup" and self.timer_active and self.pintu_tutup_zone_name in zones_after: self.handle_pintu_tutup(camera_name) except Exception as e: From ebb162231408fc1bf2aa4acf19cf2c80d1f926c3 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Thu, 5 Mar 2026 16:09:31 +0700 Subject: [PATCH 3/7] Add more guards --- frigate_counter.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/frigate_counter.py b/frigate_counter.py index e6702f8..c8c59ea 100644 --- a/frigate_counter.py +++ b/frigate_counter.py @@ -35,6 +35,8 @@ class FrigateCounter: self.topic = os.environ.get('TOPIC', f"{self.top_topic}/counter/{self.site_name}") self.camera_name = os.environ.get('CAMERA_NAME', 'kandang_1_karung_masuk') self.pintu_tutup_zone_name = os.environ.get('PINTU_TUTUP_ZONE_NAME', 'pintu_tutup') + self.pintu_kiri_buka_zone_name = os.environ.get('PINTU_KIRI_BUKA_ZONE_NAME', 'pintu_kiri_buka') + self.pintu_kanan_buka_zone_name = os.environ.get('PINTU_KANAN_BUKA_ZONE_NAME', 'pintu_kanan_buka') logger.info(f"FRIGATE_MQTT_HOST: {self.frigate_mqtt_host}:{self.frigate_mqtt_port}") logger.info(f"REPORT_MQTT_HOST: {self.report_mqtt_host}:{self.report_mqtt_port}") @@ -63,6 +65,8 @@ class FrigateCounter: self.timer_active_pintu = False self.timer_start_time_pintu = None + self.pintu_buka_timer = False + # Load previous counter value on startup self.load_previous_counter() @@ -174,13 +178,13 @@ class FrigateCounter: logger.debug(f"Received message: camera={camera_name}, type={event_type}, label={label}") # Handle different object types - if label == "pintu-kiri-buka" and not self.timer_active and not self.pintu_kiri_buka_detected: + if label == "pintu-kiri-buka" and not self.timer_active and not self.pintu_kiri_buka_detected and self.pintu_kiri_buka_zone_name in zones_after: self.handle_pintu_kiri_buka(camera_name) - elif label == "pintu-kanan-buka" and not self.timer_active and not self.pintu_kanan_buka_detected: + elif label == "pintu-kanan-buka" and not self.timer_active and not self.pintu_kanan_buka_detectedi and self.pintu_kanan_buka_zone_name in zones_after: self.handle_pintu_kanan_buka(camera_name) elif label == "karung" and self.timer_active: self.handle_karung(camera_name, track_id) - elif label == "pintu-tutup" and self.timer_active and self.pintu_tutup_zone_name in zones_after: + elif label == "pintu-tutup" and self.timer_active and self.pintu_tutup_zone_name in zones_after and not self.pintu_buka_timer: self.handle_pintu_tutup(camera_name) except Exception as e: @@ -258,11 +262,12 @@ class FrigateCounter: logger.info("Start Counting...") self.timer_active = True self.timer_start_time = datetime.now() + self.pintu_buka_timer = True # Schedule timer expiration check - #timer_thread = threading.Thread(target=self.check_timer_expiration) - #timer_thread.daemon = True - #timer_thread.start() + timer_thread = threading.Thread(target=self.check_timer_expiration_pintu_buka) + timer_thread.daemon = True + timer_thread.start() # Reset detection flags self.pintu_kiri_buka_detected = False @@ -278,15 +283,13 @@ class FrigateCounter: self.pintu_kiri_buka_detected = False self.pintu_kanan_buka_detected = False - def check_timer_expiration(self): + def check_timer_expiration_pintu_buka(self): """Check if timer has expired (60 minutes)""" - time.sleep(60 * 60) # Wait 60 minutes + time.sleep(30 * 1) # Wait 30 seconds - if self.timer_active: - logger.info("Timer expired (60 minutes)") - self.timer_active = False - #self.publish_result() - #self.reset_counter() + if self.pintu_buka_timer: + logger.info("Timer Pintu Buka expired (30 seconds)") + self.pintu_buka_timer = False def publish_result(self): """Publish counter result to MQTT topic""" From 2d0c41f426490df5f5aaf76cd51a11c65b7bdec4 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Thu, 5 Mar 2026 16:16:37 +0700 Subject: [PATCH 4/7] Wording --- frigate_counter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frigate_counter.py b/frigate_counter.py index c8c59ea..a8c0c30 100644 --- a/frigate_counter.py +++ b/frigate_counter.py @@ -259,7 +259,7 @@ class FrigateCounter: def start_timer(self): """Start Counting""" #logger.info("Starting 60-minute timer") - logger.info("Start Counting...") + logger.info("Start Counting and Timer pintu-buka 30 seconds") self.timer_active = True self.timer_start_time = datetime.now() self.pintu_buka_timer = True From 3177c4095764a0b15a7f82e9d0e2796a1f2272b4 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Thu, 5 Mar 2026 19:22:36 +0700 Subject: [PATCH 5/7] Fix bug --- frigate_counter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frigate_counter.py b/frigate_counter.py index a8c0c30..e27a325 100644 --- a/frigate_counter.py +++ b/frigate_counter.py @@ -180,7 +180,7 @@ class FrigateCounter: # Handle different object types if label == "pintu-kiri-buka" and not self.timer_active and not self.pintu_kiri_buka_detected and self.pintu_kiri_buka_zone_name in zones_after: self.handle_pintu_kiri_buka(camera_name) - elif label == "pintu-kanan-buka" and not self.timer_active and not self.pintu_kanan_buka_detectedi and self.pintu_kanan_buka_zone_name in zones_after: + elif label == "pintu-kanan-buka" and not self.timer_active and not self.pintu_kanan_buka_detected and self.pintu_kanan_buka_zone_name in zones_after: self.handle_pintu_kanan_buka(camera_name) elif label == "karung" and self.timer_active: self.handle_karung(camera_name, track_id) From 7990c423cb31cccb289eac34054e00c12f1fe335 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Fri, 6 Mar 2026 09:52:44 +0700 Subject: [PATCH 6/7] Publish Result before reset counter --- frigate_counter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frigate_counter.py b/frigate_counter.py index e27a325..8192050 100644 --- a/frigate_counter.py +++ b/frigate_counter.py @@ -401,6 +401,7 @@ class FrigateCounter: # self.save_to_database() # self.save_to_json(self.camera_name) # Save current counter value before resetting + self.publish_result() self.save_to_database() self.counter = 0 self.save_to_json(self.camera_name) From e8bf41ab6b1c8e6bb467f0dd593b0ac7455d45e4 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Fri, 6 Mar 2026 13:51:55 +0700 Subject: [PATCH 7/7] Remove zone pintu kiri buka and pintu kanan buka --- frigate_counter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frigate_counter.py b/frigate_counter.py index 8192050..ce81f20 100644 --- a/frigate_counter.py +++ b/frigate_counter.py @@ -178,9 +178,11 @@ class FrigateCounter: logger.debug(f"Received message: camera={camera_name}, type={event_type}, label={label}") # Handle different object types - if label == "pintu-kiri-buka" and not self.timer_active and not self.pintu_kiri_buka_detected and self.pintu_kiri_buka_zone_name in zones_after: + #if label == "pintu-kiri-buka" and not self.timer_active and not self.pintu_kiri_buka_detected and self.pintu_kiri_buka_zone_name in zones_after: + if label == "pintu-kiri-buka" and not self.timer_active and not self.pintu_kiri_buka_detected: self.handle_pintu_kiri_buka(camera_name) - elif label == "pintu-kanan-buka" and not self.timer_active and not self.pintu_kanan_buka_detected and self.pintu_kanan_buka_zone_name in zones_after: + #elif label == "pintu-kanan-buka" and not self.timer_active and not self.pintu_kanan_buka_detected and self.pintu_kanan_buka_zone_name in zones_after: + elif label == "pintu-kanan-buka" and not self.timer_active and not self.pintu_kanan_buka_detected: self.handle_pintu_kanan_buka(camera_name) elif label == "karung" and self.timer_active: self.handle_karung(camera_name, track_id)