First Commit

This commit is contained in:
2026-02-14 17:00:37 +07:00
parent 87168864e3
commit 6ada707d98
6 changed files with 702 additions and 0 deletions

View File

@@ -1,2 +1,6 @@
# frigate_counter
to be installed in /etc/frigate
apt install python3-flask
frigate_counter.py -> as frigate_counter service

337
frigate_counter.py Normal file
View File

@@ -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()

15
frigate_counter.service Normal file
View File

@@ -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

175
frigate_counter_web.py Normal file
View File

@@ -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)

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
flask

170
templates/index.html Normal file
View File

@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Frigate Counter Data</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
.summary {
background-color: #e8f4f8;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background-color: #4CAF50;
color: white;
}
tr:hover {
background-color: #f5f5f5;
}
.refresh-btn {
background-color: #4CAF50;
color: white;
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
margin-bottom: 20px;
}
.refresh-btn:hover {
background-color: #45a049;
}
.last-updated {
text-align: center;
color: #666;
font-style: italic;
margin-top: 10px;
}
.matrix-table {
margin-top: 20px;
}
.matrix-table th {
background-color: #2196F3;
}
.matrix-table td {
text-align: center;
}
.matrix-table tr:nth-child(even) {
background-color: #f2f2f2;
}
.matrix-table tr:nth-child(odd) {
background-color: #ffffff;
}
</style>
</head>
<body>
<div class="container">
<h1>Frigate Counter Data</h1>
<div class="summary">
<h2>Counter Summary</h2>
<table id="summary-table">
<thead>
<tr>
<th>Camera Name</th>
<th>Latest Date</th>
<th>Counter Value</th>
</tr>
</thead>
<tbody>
{% for item in summary_data %}
<tr>
<td>{{ item.camera_name }}</td>
<td>{{ item.latest_date }}</td>
<td>{{ item.counter_value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div>
<h2>Counter Data by Camera and Date</h2>
<table class="matrix-table">
<thead>
<tr>
<th>Camera Name</th>
{% for date in pivoted_data.dates %}
<th>{{ date }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in pivoted_data.matrix %}
<tr>
<td>{{ row.camera_name }}</td>
{% for date in pivoted_data.dates %}
<td>{{ row[date] }}</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div>
<h2>Full Counter Data</h2>
<table id="data-table">
<thead>
<tr>
<th>ID</th>
<th>Camera Name</th>
<th>Date</th>
<th>Counter Value</th>
</tr>
</thead>
<tbody>
{% for item in counter_data %}
<tr>
<td>{{ item.id }}</td>
<td>{{ item.camera_name }}</td>
<td>{{ item.date }}</td>
<td>{{ item.counter_value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="last-updated">
Last updated: {{ current_time.strftime('%Y-%m-%d %H:%M:%S') }}
</div>
</div>
<script>
// Auto-refresh every 30 seconds
setInterval(function() {
location.reload();
}, 30000);
</script>
</body>
</html>