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
+
+
+
+ | Camera Name |
+ Latest Date |
+ Counter Value |
+
+
+
+ {% for item in summary_data %}
+
+ | {{ item.camera_name }} |
+ {{ item.latest_date }} |
+ {{ item.counter_value }} |
+
+ {% endfor %}
+
+
+
+
+
+
Counter Data by Camera and Date
+
+
+
+ | Camera Name |
+ {% for date in pivoted_data.dates %}
+ {{ date }} |
+ {% endfor %}
+
+
+
+ {% for row in pivoted_data.matrix %}
+
+ | {{ row.camera_name }} |
+ {% for date in pivoted_data.dates %}
+ {{ row[date] }} |
+ {% endfor %}
+
+ {% endfor %}
+
+
+
+
+
+
Full Counter Data
+
+
+
+ | ID |
+ Camera Name |
+ Date |
+ Counter Value |
+
+
+
+ {% for item in counter_data %}
+
+ | {{ item.id }} |
+ {{ item.camera_name }} |
+ {{ item.date }} |
+ {{ item.counter_value }} |
+
+ {% endfor %}
+
+
+
+
+
+ Last updated: {{ current_time.strftime('%Y-%m-%d %H:%M:%S') }}
+
+
+
+
+
+
\ No newline at end of file