diff --git a/counter_snapshot.ds b/counter_snapshot.ds new file mode 100755 index 0000000..418d686 --- /dev/null +++ b/counter_snapshot.ds @@ -0,0 +1,9 @@ +#!/bin/bash + +WORKDIR=/etc/frigate +PYTHON_EXEC=${WORKDIR}/counter_snapshot.py + +CURRENT_DATE=`date +%Y-%m-%d` +CAMERA_NAME="kandang_1_karung_pakan" + +${PYTHON_EXEC} ${CAMERA_NAME} ${CURRENT_DATE} diff --git a/counter_snapshot.py b/counter_snapshot.py new file mode 100755 index 0000000..0420b72 --- /dev/null +++ b/counter_snapshot.py @@ -0,0 +1,177 @@ +#!/usr/bin/python3 + +import sys +import sqlite3 +from pathlib import Path +from datetime import datetime, timedelta + +def list_files_with_extension(directory_path, extension): + """ + Lists files in a directory with a specific extension using pathlib.Path.glob(). + + Args: + directory_path (str): The path to the directory. + extension (str): The desired file extension (e.g., '.txt', '.py'). + + Returns: + list: A list of Path objects for files with the specified extension. + """ + path_obj = Path(directory_path) + file_list = [f.name for f in path_obj.iterdir() if f.is_file() and f.suffix == extension] + return file_list + +def list_files_pathlib(directory_path): + """Lists all files in a given directory using the pathlib module.""" + files_list = [] + path = Path(directory_path) + for item in path.iterdir(): + if item.is_file(): + files_list.append(item.name) # Appends only the file name + return files_list + +def get_start_end_timestamp(data_date: str = None) -> [int, int]: + start_ts = end_ts = 0 + if data_date: + format_string = "%Y-%m-%d" + try: + end_date = datetime.strptime(data_date, format_string) + end_time = end_date.replace(hour=17, minute=0, second=0) + start_time = end_time - timedelta(days=1) + start_ts = int(start_time.timestamp()) + end_ts = int(end_time.timestamp()) + + except ValueError: + print(f"Value {start_date} must be in format '%Y-%m-%d'") + sys.exit(1) + + else: + start_time = end_time = datetime.now() + if end_time.hour < 17: + start_time = end_time - timedelta(days=1) + + start_time = start_time.replace(hour=17, minute=0, second=0, microsecond=0) + + start_ts = int(start_time.timestamp()) + end_ts = int(end_time.timestamp()) + + return start_ts, end_ts + + +def get_ts_from_file(file: str) -> int: + file_split = file.split("-") + ts_int = int(float(file_split[1])) + + return ts_int + + +def get_files(start_ts: int = 0, end_ts: int = 0, camera_name: str = None) -> list: + directory = "/var/lib/docker/volumes/frigate_storage/_data/clips" # Current directory + files = list_files_with_extension(directory, ".jpg") + + files_list = [] + for file in files: + ts_int = get_ts_from_file(file) + if ts_int >= start_ts: + if end_ts == 0: + if camera_name in file: + files_list.append(file) + elif ts_int < end_ts: + if camera_name in file: + files_list.append(file) + + return files_list + + +def check_duplicate(files: list = []) -> list: + DETIK_ANTAR_KARUNG = 30 + files_nodup = [] + for i in range(len(files)-1): + ts_current = get_ts_from_file(files[i]) + ts_next = get_ts_from_file(files[i+1]) + if ts_next - ts_current > DETIK_ANTAR_KARUNG: + files_nodup.append(files[i]) + + return files_nodup + + +def store_in_db(date_ts: int, camera_name: str, value: int, value_nodup: int): + + print(str(datetime.now()), f"Get {date_ts} {camera_name} {value} {value_nodup}") + DB_FILE = "/etc/frigate/counter.db" + conn = sqlite3.connect(DB_FILE) + cursor = conn.cursor() + + # Create DB + create_sql = '''CREATE TABLE IF NOT EXISTS counter ( + id INTEGER PRIMARY KEY, + date_ts INTEGER, + camera_name TEXT, + value INTEGER, + value_nodup INTEGER + ) + ''' + + cursor.execute(create_sql) + + # Start Checkin DB + allrows = [] + try: + cursor.execute("SELECT * FROM counter where date_ts=? and camera_name=?", (date_ts, camera_name)) + allrows = cursor.fetchall() + + except sqlite3.OperationalError: + print(str(datetime.now()), f"Error") + + if len(allrows): + # UPDATE + print(str(datetime.now()), f"Found record. Update DB") + update_sql = "UPDATE counter SET value = ?, value_nodup = ? WHERE date_ts = ? and camera_name = ?" + cursor.execute(update_sql, (date_ts, camera_name, value, value_nodup)) + + else: + # INSERT + print(str(datetime.now()), f"Insert into DB") + insert_sql = "INSERT INTO counter (date_ts, camera_name, value, value_nodup) VALUES (?, ?, ?, ?)" + cursor.execute(insert_sql, (date_ts, camera_name, value, value_nodup)) + + conn.commit() + conn.close() + + +end_date = None + +if len(sys.argv) > 2: + camera_name = sys.argv[1] + end_date = sys.argv[2] + start_ts, end_ts = get_start_end_timestamp(end_date) +elif len(sys.argv) > 1: + camera_name = sys.argv[1] + start_ts, end_ts = get_start_end_timestamp() +else: + print("================") + print("Counter Snapshot") + print("================\n") + print(f"Usage:") + print(f"\tpython3 {sys.argv[0]} [camera_name] [data_date]\n") + print(f"Example:") + print(f"\tpython3 {sys.argv[0]} kandang_1_karung_pakan") + print(f"\tpython3 {sys.argv[0]} kandang_1_karung_pakan 2025-09-23 -> data from 2025-09-22 17:00:00 to 2025-09-23 16:59:59") + sys.exit(1) + +#print(start_ts, end_ts, camera_name) +files = get_files(start_ts, end_ts, camera_name) + +files_nodup = check_duplicate(files) + +value = len(files) +value_nodup = len(files_nodup) +#print(value) +#print(value_nodup) + +if end_date: + store_in_db(end_ts, camera_name, value, value_nodup) + print(str(datetime.now()), f"Store in DB {end_ts}/{camera_name}/{value}/{value_nodup}") + +#for file in files_nodup: +# print(file) + diff --git a/frigate_counter.py b/frigate_counter.py new file mode 100644 index 0000000..aa9df3c --- /dev/null +++ b/frigate_counter.py @@ -0,0 +1,340 @@ +#!/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 = "localhost" +PUBLISH_BROKER = "mqtt.backone.cloud" +PUBLISH_PORT = 1883 +USERNAME = "" # ganti user +PASSWORD = "" # ganti password + +# ===================== +# FILE COUNTER +# ===================== +DATA_FILE = "/etc/frigate-counter/karung-tuang/karung_tuang.json" + +# ===================== +# DATABASE INITIALIZATION +# ===================== +DB_FILE = "/etc/frigate-counter/karung-tuang/karung_tuang.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_atas_feeder_kiri": {"karung": 0}, + "kandang_bawah_feeder_kanan": {"karung": 0}, + } + with open(DATA_FILE, "w") as f: + json.dump(counter, f, indent=2) + +camera_feeder_no_duplicate = [ + "kandang_atas_feeder_kiri", + "kandang_bawah_feeder_kanan" + ] + +camera_feeder = [ + "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] + + # DEDY Don't count camera_masuk. Only count camera feeder + #if camera not in camera_feeder: + # return + + 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: + DETIK_ANTAR_KARUNG = 35 + 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() + #save_counter_to_database(camera_feeder) + # 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/karung_tuang.db b/karung_tuang.db new file mode 100644 index 0000000..836890c Binary files /dev/null and b/karung_tuang.db differ diff --git a/karung_tuang.json b/karung_tuang.json new file mode 100644 index 0000000..22e5217 --- /dev/null +++ b/karung_tuang.json @@ -0,0 +1,8 @@ +{ + "kandang_atas_feeder_kiri": { + "karung": 12 + }, + "kandang_bawah_feeder_kanan": { + "karung": 16 + } +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..44dbe40 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,170 @@ + + + + + + Frigate Counter Karung Tuang Feeder + + + +
+

Frigate Counter Karung Tuang Feeder

+ +
+

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') }} +
+
+ + + + diff --git a/web_app.py b/web_app.py new file mode 100644 index 0000000..5e77668 --- /dev/null +++ b/web_app.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)