From 6ada707d9869e278b5e193de26bb9097b3af92b2 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Sat, 14 Feb 2026 17:00:37 +0700 Subject: [PATCH] First Commit --- README.md | 4 + frigate_counter.py | 337 ++++++++++++++++++++++++++++++++++++++++ frigate_counter.service | 15 ++ frigate_counter_web.py | 175 +++++++++++++++++++++ requirements.txt | 1 + templates/index.html | 170 ++++++++++++++++++++ 6 files changed, 702 insertions(+) create mode 100644 frigate_counter.py create mode 100644 frigate_counter.service create mode 100644 frigate_counter_web.py create mode 100644 requirements.txt create mode 100644 templates/index.html diff --git a/README.md b/README.md index feed23b..50fbe2a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # frigate_counter + to be installed in /etc/frigate +apt install python3-flask + +frigate_counter.py -> as frigate_counter service diff --git a/frigate_counter.py b/frigate_counter.py new file mode 100644 index 0000000..a678f2a --- /dev/null +++ b/frigate_counter.py @@ -0,0 +1,337 @@ +#!/usr/bin/env python3 +import json +import threading +import paho.mqtt.client as mqtt +import os +import sqlite3 +from datetime import datetime, timedelta + +# ===================== +# SITE CONFIG +# ===================== +TOP_TOPIC = "cpsp" +SITE_NAME = "sukawarna" +SITE_TOPIC = f"{TOP_TOPIC}/counter/{SITE_NAME}" + +# ===================== +# KONFIGURASI MQTT +# ===================== +BROKER = "localhost" # ganti sesuai broker +PORT = 1883 +USERNAME = "" # ganti user +PASSWORD = "" # ganti password +TOPIC = "frigate/events" + +# ===================== +# MQTT PUBLISH +# ===================== +PUBLISH_BROKER = "mqtt.backone.cloud" +PUBLISH_PORT = 1883 +USERNAME = "" # ganti user +PASSWORD = "" # ganti password + +# ===================== +# FILE COUNTER +# ===================== +DATA_FILE = "/etc/frigate/counter_data.json" + +# ===================== +# DATABASE INITIALIZATION +# ===================== +DB_FILE = "/etc/frigate/counter_database.db" + + +def init_database(): + """Initialize the SQLite database with the required table""" + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + + # Create table if it doesn't exist + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS counter_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + camera_name TEXT NOT NULL, + date TIMESTAMP NOT NULL, + counter_value INTEGER NOT NULL + ) + """ + ) + + conn.commit() + conn.close() + + +# Initialize the database +init_database() + +# ===================== +# INISIALISASI COUNTER +# ===================== +if os.path.exists(DATA_FILE): + with open(DATA_FILE, "r") as f: + counter = json.load(f) +else: + counter = { + "kandang_1_karung_pakan": {"karung": 0}, + "kandang_2_karung_pakan": {"karung": 0}, + "kandang_1_karung_masuk": {"karung": 0}, + "kandang_atas_feeder_kiri": {"karung": 0}, + "kandang_bawah_feeder_kanan": {"karung": 0}, + } + +camera_feeder_no_duplicate = [ + "kandang_1_karung_pakan", + "kandang_2_karung_pakan" + ] + +camera_feeder = [ + "kandang_1_karung_pakan", + "kandang_2_karung_pakan", + "kandang_atas_feeder_kiri", + "kandang_bawah_feeder_kanan" + ] + +camera_masuk = ["kandang_1_karung_masuk"] + +# Untuk debounce object per track_id +seen_objects = {} # dict per kamera: {track_id: last_seen} + + +# ===================== +# FUNGSI SAVE COUNTER +# ===================== +def save_counter(): + with open(DATA_FILE, "w") as f: + json.dump(counter, f, indent=2) + + +def save_counter_to_database(camera_list_to_save=[]): + """Save current counter values to SQLite database""" + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + + # Get current timestamp + current_time = datetime.now() + + # Save each counter value to database + for camera_name, camera_data in counter.items(): + for label, value in camera_data.items(): + if label == "karung" and camera_name in camera_list_to_save: # Only save karung counters + cursor.execute( + """ + INSERT INTO counter_data (camera_name, date, counter_value) + VALUES (?, ?, ?) + """, + (camera_name, current_time, value), + ) + + conn.commit() + conn.close() + + +def republish_counter(): + # Publish current counter + for camera, v in counter.items(): + for label, value in v.items(): + if label == "karung": + print(f"{SITE_TOPIC}/{camera}/{label}", value) + client_publish.publish( + f"{SITE_TOPIC}/{camera}/{label}", value, qos=1, retain=True + ) + + +# ===================== +# RESET HARIAN +# ===================== +def reset_counter(): + global counter, seen_objects + print(f"[{datetime.now()}] Reset counter otomatis") + # Save current counter values to database before reset + save_counter_to_database(camera_feeder) + # reset semua + # for cam in counter: + # for obj in counter[cam]: + # counter[cam][obj] = 0 + for camera_name in camera_feeder: + counter[camera_name]["karung"] = 0 + + """ + counter["kandang_1_karung_pakan"]["karung"] = 0 + counter["kandang_2_karung_pakan"]["karung"] = 0 + counter["kandang_atas_feeder_kiri"]["karung"] = 0 + counter["kandang_bawah_feeder_kanan"]["karung"] = 0 + """ + seen_objects = {} + save_counter() + schedule_reset() # jadwalkan besok + republish_counter() + + +def reset_counter_masuk(): + global counter, seen_objects + print(f"[{datetime.now()}] Reset counter otomatis") + # Save current counter values to database before reset + save_counter_to_database(camera_masuk) + # reset semua + # for cam in counter: + # for obj in counter[cam]: + # counter[cam][obj] = 0 + for camera_name in camera_masuk: + counter[camera_name]["karung"] = 0 + + #counter["kandang_1_karung_masuk"]["karung"] = 0 + + seen_objects = {} + save_counter() + schedule_reset_masuk() # jadwalkan besok + republish_counter() + + +def schedule_reset(): + now = datetime.now() + reset_time = now.replace(hour=17, minute=0, second=0, microsecond=0) + if now >= reset_time: + reset_time += timedelta(days=1) + delay = (reset_time - now).total_seconds() + threading.Timer(delay, reset_counter).start() + print(f"Reset counter dijadwalkan pada: {reset_time}") + + +def schedule_reset_masuk(): + now = datetime.now() + reset_time = now.replace(hour=23, minute=59, second=55, microsecond=0) + if now >= reset_time: + reset_time += timedelta(days=1) + delay = (reset_time - now).total_seconds() + threading.Timer(delay, reset_counter_masuk).start() + print(f"Reset counter dijadwalkan pada: {reset_time}") + + +# ===================== +# CALLBACK MQTT +# ===================== +def on_connect(client, userdata, flags, rc): + print("Connected to MQTT with result code " + str(rc)) + client.subscribe(TOPIC) + + +def on_connect_publish(client, userdata, flags, rc): + print("Connected to MQTT PUBLISH with result code " + str(rc)) + # Publish current counter + republish_counter() + + +def on_message(client, userdata, msg): + try: + payload = json.loads(msg.payload.decode()) + if "after" not in payload: + return + + event_after = payload["after"] + event_before = payload.get("before", {}) + + camera = event_after.get("camera") + label = event_after.get("label") + track_id = event_after.get("id") + zones_after = event_after.get("entered_zones", []) + zones_before = event_before.get("entered_zones", []) + + # Dont detect stationary + stationary = event_after.get("stationary") + if stationary: + return + + # ambil zona baru yg sebelumnya belum ada + new_zones = [z for z in zones_after if z not in zones_before] + + if not camera or not label: + return + if camera not in counter or label not in counter[camera]: + return + if not new_zones: # hanya hitung kalau masuk zona baru + return + + # debounce per track_id + if camera not in seen_objects: + seen_objects[camera] = {} + if track_id in seen_objects[camera]: + return # sudah dihitung + + # Check if timestamp is more than 30 seconds + # if camera == "kandang_1_karung_pakan" or camera == "kandang_2_karung_pakan": + """ + if ( + camera == "kandang_1_karung_pakan" + or camera == "kandang_2_karung_pakan" + or camera == "kandang_atas_feeder_kiri" + or camera == "kandang_bawah_feeder_kanan" + ): + """ + if camera in camera_feeder_no_duplicate: + DETIK_ANTAR_KARUNG = 30 + current_time = datetime.now() + for ts in seen_objects[camera].values(): + delta = current_time - ts + if delta.seconds < DETIK_ANTAR_KARUNG: + return + + # hitung + counter[camera][label] += 1 + seen_objects[camera][track_id] = datetime.now() + + print( + f"[{datetime.now()}] Kamera {camera}: Object {label} masuk zona {new_zones}" + ) + print(f"Total {label} di {camera}: {counter[camera][label]}") + + print(f"{TOP_TOPIC}/counter/{SITE_NAME}/{camera}/{label}") + + # simpan dan publish + save_counter() + # client_publish.publish(f"{TOP_TOPIC}/counter/{SITE_NAME}/{camera}/{label}", counter[camera][label], qos=1, retain=True) + client_publish.publish( + f"{SITE_TOPIC}/{camera}/{label}", counter[camera][label], qos=1, retain=True + ) + + except Exception as e: + print("Error parsing message:", e) + + +def view_database(): + """View all records in the database""" + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + + cursor.execute("SELECT * FROM counter_data ORDER BY date DESC LIMIT 10") + rows = cursor.fetchall() + + print("Last 10 records in database:") + for row in rows: + print(f"ID: {row[0]}, Camera: {row[1]}, Date: {row[2]}, Counter: {row[3]}") + + conn.close() + +# ===================== +# MAIN +# ===================== +client = mqtt.Client() +client.username_pw_set(USERNAME, PASSWORD) +client.on_connect = on_connect +client.on_message = on_message + +client.connect(BROKER, PORT, 60) + +client_publish = mqtt.Client() +client_publish.on_connect = on_connect_publish +client_publish.connect(PUBLISH_BROKER, PUBLISH_PORT, 60) + + +# jadwalkan reset pertama +schedule_reset() +schedule_reset_masuk() + +client_publish.loop_start() +print(f"MQTT Publish Counter berjalan di {PUBLISH_BROKER}") +print(f"MQTT Counter berjalan di {BROKER}") +client.loop_forever() diff --git a/frigate_counter.service b/frigate_counter.service new file mode 100644 index 0000000..95a7036 --- /dev/null +++ b/frigate_counter.service @@ -0,0 +1,15 @@ +[Unit] +Description=Frigate MQTT Counter +After=network.target + +[Service] +ExecStart=/usr/bin/python3 -u /etc/frigate/frigate_counter.py +WorkingDirectory=/etc/frigate +StandardOutput=journal +StandardError=journal +Restart=always +RestartSec=5s + +[Install] +WantedBy=multi-user.target + diff --git a/frigate_counter_web.py b/frigate_counter_web.py new file mode 100644 index 0000000..5e77668 --- /dev/null +++ b/frigate_counter_web.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +import sqlite3 +from flask import Flask, render_template, jsonify +from datetime import datetime +import os +from collections import defaultdict + +# Configuration +DB_FILE = "/etc/frigate/counter_database.db" + +app = Flask(__name__) + +def get_db_connection(): + """Create a database connection""" + conn = sqlite3.connect(DB_FILE) + conn.row_factory = sqlite3.Row # This allows us to access columns by name + return conn + +def get_counter_data(): + """Get all counter data from the database""" + conn = get_db_connection() + cursor = conn.cursor() + + # Get all records ordered by date descending + cursor.execute(""" + SELECT id, camera_name, date, counter_value + FROM counter_data + ORDER BY date DESC + """) + + rows = cursor.fetchall() + conn.close() + + # Convert to list of dictionaries + data = [] + for row in rows: + data.append({ + 'id': row['id'], + 'camera_name': row['camera_name'], + 'date': row['date'], + 'counter_value': row['counter_value'] + }) + + return data + +def get_counter_summary(): + """Get summary of counter data grouped by camera""" + conn = get_db_connection() + cursor = conn.cursor() + + # Get latest counter value for each camera + cursor.execute(""" + SELECT camera_name, MAX(date) as latest_date, counter_value + FROM counter_data + GROUP BY camera_name + ORDER BY latest_date DESC + """) + + rows = cursor.fetchall() + conn.close() + + # Convert to list of dictionaries + data = [] + for row in rows: + data.append({ + 'camera_name': row['camera_name'], + 'latest_date': row['latest_date'], + 'counter_value': row['counter_value'] + }) + + return data + +def get_pivoted_data(): + """Get counter data pivoted by camera_name (rows) and date (columns)""" + conn = get_db_connection() + cursor = conn.cursor() + + # Get all records ordered by date DESC (latest first) + cursor.execute(""" + SELECT camera_name, date, counter_value + FROM counter_data + ORDER BY date DESC + """) + + rows = cursor.fetchall() + conn.close() + + # Group data by camera and date + pivoted_data = defaultdict(lambda: defaultdict(int)) + dates = set() + cameras = set() + + for row in rows: + camera_name = row['camera_name'] + date = row['date'][:10] # Extract just the date part (YYYY-MM-DD) + counter_value = row['counter_value'] + + pivoted_data[camera_name][date] = counter_value + dates.add(date) + cameras.add(camera_name) + + # Convert sets to sorted lists (sort dates in reverse order - latest first) + sorted_dates = sorted(dates, reverse=True) + sorted_cameras = sorted(cameras) + + # Create matrix format + matrix = [] + for camera in sorted_cameras: + row = {'camera_name': camera} + for date in sorted_dates: + row[date] = pivoted_data[camera][date] + matrix.append(row) + + return { + 'matrix': matrix, + 'dates': sorted_dates, + 'cameras': sorted_cameras + } + +@app.route('/') +def index(): + """Main page showing counter data""" + counter_data = get_counter_data() + summary_data = get_counter_summary() + pivoted_data = get_pivoted_data() + + # Add current datetime to context + current_time = datetime.now() + + return render_template('index.html', + counter_data=counter_data, + summary_data=summary_data, + pivoted_data=pivoted_data, + current_time=current_time) + +@app.route('/api/data') +def api_data(): + """API endpoint to get counter data as JSON""" + counter_data = get_counter_data() + return jsonify(counter_data) + +@app.route('/api/summary') +def api_summary(): + """API endpoint to get counter summary as JSON""" + summary_data = get_counter_summary() + return jsonify(summary_data) + +@app.route('/api/pivoted') +def api_pivoted(): + """API endpoint to get pivoted counter data as JSON""" + pivoted_data = get_pivoted_data() + return jsonify(pivoted_data) + +if __name__ == '__main__': + + # Create database directory if it doesn't exist + os.makedirs(os.path.dirname(DB_FILE), exist_ok=True) + + # Initialize the database if needed + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS counter_data ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + camera_name TEXT NOT NULL, + date TIMESTAMP NOT NULL, + counter_value INTEGER NOT NULL + ) + """) + + conn.commit() + conn.close() + + app.run(host='0.0.0.0', port=8899, debug=True) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7e10602 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +flask diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..762134c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,170 @@ + + + + + + Frigate Counter Data + + + +
+

Frigate Counter Data

+ +
+

Counter Summary

+ + + + + + + + + + {% for item in summary_data %} + + + + + + {% endfor %} + +
Camera NameLatest DateCounter Value
{{ item.camera_name }}{{ item.latest_date }}{{ item.counter_value }}
+
+ +
+

Counter Data by Camera and Date

+ + + + + {% for date in pivoted_data.dates %} + + {% endfor %} + + + + {% for row in pivoted_data.matrix %} + + + {% for date in pivoted_data.dates %} + + {% endfor %} + + {% endfor %} + +
Camera Name{{ date }}
{{ row.camera_name }}{{ row[date] }}
+
+ +
+

Full Counter Data

+ + + + + + + + + + + {% for item in counter_data %} + + + + + + + {% endfor %} + +
IDCamera NameDateCounter Value
{{ item.id }}{{ item.camera_name }}{{ item.date }}{{ item.counter_value }}
+
+ +
+ Last updated: {{ current_time.strftime('%Y-%m-%d %H:%M:%S') }} +
+
+ + + + \ No newline at end of file