From c52d608d5855d1dcf78bb7b2abfa5c2ec727bff2 Mon Sep 17 00:00:00 2001 From: dsutanto Date: Tue, 7 Apr 2026 16:08:03 +0700 Subject: [PATCH] Update frigate_counter to GPIO --- frigate-counter.service | 23 -- frigate-counter_karung-masuk.service | 23 ++ frigate_counter.py | 75 +++-- frigate_counter.py.backup | 433 +++++++++++++++++++++++++++ karung_counts.db => karung_masuk.db | Bin 12288 -> 12288 bytes karung_masuk.json | 5 + templates/base.html | 222 ++++++++++++++ templates/camera_detail.html | 52 ++++ templates/date_detail.html | 58 ++++ templates/index.html | 123 ++++++++ web_app.py | 221 ++++++++++++++ 11 files changed, 1194 insertions(+), 41 deletions(-) delete mode 100644 frigate-counter.service create mode 100644 frigate-counter_karung-masuk.service create mode 100644 frigate_counter.py.backup rename karung_counts.db => karung_masuk.db (92%) create mode 100644 karung_masuk.json 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/frigate-counter.service b/frigate-counter.service deleted file mode 100644 index 3bf8046..0000000 --- a/frigate-counter.service +++ /dev/null @@ -1,23 +0,0 @@ -[Unit] -Description=Frigate MQTT Counter Service -After=network.target - -[Service] -Type=simple -User=frigate -Group=frigate -WorkingDirectory=/Users/dsutanto/MyEnv/Codes/CLAUDE/karung-masuk-frigate-counter -ExecStart=/Users/dsutanto/MyEnv/Codes/CLAUDE/karung-masuk-frigate-counter/venv/bin/python /Users/dsutanto/MyEnv/Codes/CLAUDE/karung-masuk-frigate-counter/frigate_counter.py -Restart=always -RestartSec=10 - -# Environment variables for MQTT configuration -Environment=FRIGATE_MQTT_HOST=localhost -Environment=FRIGATE_MQTT_PORT=1883 -Environment=REPORT_MQTT_HOST=localhost -Environment=REPORT_MQTT_PORT=1883 -Environment=TOPIC=frigate -Environment=SITE_NAME=default - -[Install] -WantedBy=multi-user.target \ No newline at end of file diff --git a/frigate-counter_karung-masuk.service b/frigate-counter_karung-masuk.service new file mode 100644 index 0000000..ba4e3ae --- /dev/null +++ b/frigate-counter_karung-masuk.service @@ -0,0 +1,23 @@ +[Unit] +Description=Frigate MQTT Counter Service +After=network.target + +[Service] +Type=simple +User=frigate +Group=frigate +WorkingDirectory=/etc/frigate-counter/karung-masuk +ExecStart=/usr/bin/python3 /etc/frigate-counter/karung-masuk/frigate_counter.py +Restart=always +RestartSec=10 + +# Environment variables for MQTT configuration +#Environment=FRIGATE_MQTT_HOST=localhost +#Environment=FRIGATE_MQTT_PORT=1883 +#Environment=REPORT_MQTT_HOST=localhost +#Environment=REPORT_MQTT_PORT=1883 +#Environment=TOPIC=frigate +#Environment=SITE_NAME=default + +[Install] +WantedBy=multi-user.target diff --git a/frigate_counter.py b/frigate_counter.py index 3a21f25..2b7b78b 100644 --- a/frigate_counter.py +++ b/frigate_counter.py @@ -13,6 +13,7 @@ import threading import os import logging import json +import requests from datetime import datetime, date from typing import Optional @@ -28,25 +29,32 @@ 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', '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') + 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}") logger.info(f"TOPIC: {self.topic}") logger.info(f"CAMERA_NAME: {self.camera_name}") + # Webcall to RPi + self.relay_on = os.environ.get('RELAY_ON_URI', 'http://192.168.192.26:5000/relay_on') + self.relay_off = os.environ.get('RELAY_OFF_URI', 'http://192.168.192.26:5000/relay_off') # Database setup - self.db_path = 'karung_counts.db' + self.db_path = '/etc/frigate-counter/karung-masuk/karung_masuk.db' self.init_database() # JSON storage for temporary persistent counter values - self.json_storage_path = 'karung_counters.json' + self.json_storage_path = '/etc/frigate-counter/karung-masuk/karung_masuk.json' self.init_json_storage() # State tracking @@ -62,6 +70,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() @@ -75,6 +85,20 @@ class FrigateCounter: # Schedule daily reset at midnight schedule.every().day.at("23:59").do(self.reset_counter) + def action_relay_on(self): + try: + requests.get(self.relay_on, timeout=2) + except requests.exceptions.RequestException as e: + pass + logger.info("Relay ON") + + def action_relay_off(self): + try: + requests.get(self.relay_off, timeout=2) + except requests.exceptions.RequestException as e: + pass + logger.info("Relay OFF") + def init_database(self): """Initialize SQLite database with required table""" conn = sqlite3.connect(self.db_path) @@ -154,7 +178,8 @@ class FrigateCounter: # Dont detect stationary stationary = event_after.get("stationary") - if stationary: + #if stationary and: + if stationary and label == "karung": return new_zones = [z for z in zones_after if z not in zones_before] @@ -179,7 +204,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 and not self.pintu_buka_timer: self.handle_pintu_tutup(camera_name) except Exception as e: @@ -194,6 +219,9 @@ class FrigateCounter: self.timer_active = False self.seen_objects = {} + # Call RPi + self.action_relay_off() + def handle_pintu_kiri_buka(self, camera_name): """Handle detection of pintu-kiri-buka""" logger.info(f"Detected pintu-kiri-buka on {camera_name}") @@ -254,19 +282,23 @@ 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 5 minutes") 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 self.pintu_kanan_buka_detected = False + # Call to RPI + self.action_relay_on() + def check_timer_expiration_pintu(self): """Check if timer has expired (5 minutes)""" time.sleep(5 * 60) # Wait 5 minutes @@ -277,22 +309,28 @@ class FrigateCounter: self.pintu_kiri_buka_detected = False self.pintu_kanan_buka_detected = False - def check_timer_expiration(self): - """Check if timer has expired (60 minutes)""" - time.sleep(60 * 60) # Wait 60 minutes + # Call RPi + #if not self.timer_active: + # self.action_relay_off() - if self.timer_active: - logger.info("Timer expired (60 minutes)") - self.timer_active = False - #self.publish_result() - #self.reset_counter() + def check_timer_expiration_pintu_buka(self): + """Check if timer has expired (5 minutes)""" + time.sleep(5 * 60) # Wait 5 minutes + + if self.pintu_buka_timer: + logger.info("Timer Pintu Buka expired (5 minutes)") + self.pintu_buka_timer = False + + # Call RPi + #if not self.timer_active: + # self.action_relay_off() def publish_result(self): """Publish counter result to MQTT topic""" if self.counter > 0: topic = f"{self.topic}/{self.camera_name}/karung" #TESTING - topic = f"{self.topic}/{self.camera_name}-check_pintu/karung" + #topic = f"{self.topic}/{self.camera_name}-check_pintu/karung" message = str(self.counter) try: @@ -397,6 +435,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) diff --git a/frigate_counter.py.backup b/frigate_counter.py.backup new file mode 100644 index 0000000..01ab03c --- /dev/null +++ b/frigate_counter.py.backup @@ -0,0 +1,433 @@ +#!/usr/bin/env python3 +""" +Frigate MQTT Counter Service +Monitors Frigate NVR MQTT events to count "karung" objects +after detecting both "pintu-kiri-buka" and "pintu-kanan-buka" +""" + +import paho.mqtt.client as mqtt +import sqlite3 +import schedule +import time +import threading +import os +import logging +import json +from datetime import datetime, date +from typing import Optional + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +class FrigateCounter: + def __init__(self): + # 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') + 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}") + logger.info(f"TOPIC: {self.topic}") + logger.info(f"CAMERA_NAME: {self.camera_name}") + + + # Database setup + self.db_path = '/etc/frigate-counter/karung-masuk/karung_masuk.db' + self.init_database() + + # JSON storage for temporary persistent counter values + self.json_storage_path = '/etc/frigate-counter/karung-masuk/karung_masuk.json' + self.init_json_storage() + + # State tracking + self.pintu_kiri_buka_detected = False + self.pintu_kanan_buka_detected = False + self.timer_active = False + self.timer_start_time = None + self.counter = 0 + self.counter_lock = threading.Lock() + self.seen_objects = {} + + # State pintu tracking + 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() + + # MQTT clients + self.frigate_client = None + self.report_client = None + + # Initialize MQTT clients + self.setup_mqtt_clients() + + # Schedule daily reset at midnight + schedule.every().day.at("23:59").do(self.reset_counter) + + def init_database(self): + """Initialize SQLite database with required table""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS karung_counts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + camera_name TEXT NOT NULL, + date DATE NOT NULL, + counter_value INTEGER NOT NULL, + timestamp DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + conn.commit() + conn.close() + logger.info("Database initialized") + + def init_json_storage(self): + """Initialize JSON storage file for temporary persistent counter values""" + if not os.path.exists(self.json_storage_path): + # Create empty JSON file with empty dictionary + with open(self.json_storage_path, 'w') as f: + json.dump({}, f) + logger.info("JSON storage initialized") + + def setup_mqtt_clients(self): + """Setup MQTT clients for both Frigate and reporting""" + # Frigate MQTT client (for receiving events) + self.frigate_client = mqtt.Client() + self.frigate_client.on_connect = self.on_frigate_connect + self.frigate_client.on_message = self.on_frigate_message + self.frigate_client.connect(self.frigate_mqtt_host, self.frigate_mqtt_port, 60) + + # Reporting MQTT client (for publishing results) + self.report_client = mqtt.Client() + self.report_client.on_connect = self.on_report_connect + self.report_client.connect(self.report_mqtt_host, self.report_mqtt_port, 60) + + # Start MQTT client loops in separate threads + self.frigate_client.loop_start() + self.report_client.loop_start() + + def on_frigate_connect(self, client, userdata, flags, rc): + """Callback when connected to Frigate MQTT""" + logger.info("Connected to Frigate MQTT broker") + client.subscribe("frigate/events") + + def on_report_connect(self, client, userdata, flags, rc): + """Callback when connected to reporting MQTT broker""" + logger.info("Connected to reporting MQTT broker") + + def on_frigate_message(self, client, userdata, msg): + """Handle incoming Frigate MQTT messages""" + try: + # Parse the message (assuming JSON format) + #import json + payload = json.loads(msg.payload.decode()) + + if "after" not in payload: + return + + + #event_after = payload["after"] + event_after = payload.get("after", {}) + event_before = payload.get("before", {}) + + camera_name = event_after.get("camera", "unknown") + + if camera_name != self.camera_name: + return + + event_type = event_after.get('type', '') + 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 + + new_zones = [z for z in zones_after if z not in zones_before] + + # debounce per track_id + if camera_name not in self.seen_objects: + self.seen_objects[camera_name] = {} + if track_id in self.seen_objects[camera_name]: + return # sudah dihitung + + # Extract object type and camera name + #camera_name = data.get('camera', 'unknown') + #event_type = data.get('type', '') + #label = data.get('label', '') + + 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: + self.handle_pintu_kiri_buka(camera_name) + 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) + 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: + logger.error(f"Error processing MQTT message: {e}") + + def handle_pintu_tutup(self, camera_name): + """Handle detection of pintu-tutup""" + logger.info(f"Detected pintu-tutup {camera_name}") + + if self.timer_active: + logger.info("Stop Counting Karung") + self.timer_active = False + self.seen_objects = {} + + def handle_pintu_kiri_buka(self, camera_name): + """Handle detection of pintu-kiri-buka""" + logger.info(f"Detected pintu-kiri-buka on {camera_name}") + + # Only process if timer is not active + if not self.timer_active: + self.pintu_kiri_buka_detected = True + self.check_detection_sequence() + else: + logger.debug("Ignoring pintu-kiri-buka during timer period") + + def handle_pintu_kanan_buka(self, camera_name): + """Handle detection of pintu-kanan-buka""" + logger.info(f"Detected pintu-kanan-buka on {camera_name}") + + # Only process if timer is not active + if not self.timer_active: + self.pintu_kanan_buka_detected = True + self.check_detection_sequence() + else: + logger.debug("Ignoring pintu-kanan-buka during timer period") + + def handle_karung(self, camera_name, track_id): + """Handle detection of karung object""" + logger.info(f"Detected karung on {camera_name}") + + # Only count if timer is active + if self.timer_active: + with self.counter_lock: + self.counter += 1 + self.seen_objects[camera_name][track_id] = datetime.now() + self.save_to_json(camera_name) + self.publish_result() + logger.info(f"Counter incremented to {self.counter} and republish to MQTT") + else: + logger.debug("Ignoring karung outside timer period") + + def check_detection_sequence(self): + """Check if both pintu-kiri-buka and pintu-kanan-buka have been detected""" + if self.pintu_kiri_buka_detected and self.pintu_kanan_buka_detected: + self.start_timer() + return + + if not self.timer_active_pintu: + self.start_timer_pintu() + + def start_timer_pintu(self): + """Start the 5-minute timer Pintu""" + logger.info("Starting 5-minute timer Pintu") + self.timer_active_pintu = True + self.timer_start_time_pintu = datetime.now() + + # Schedule timer expiration check + timer_thread = threading.Thread(target=self.check_timer_expiration_pintu) + timer_thread.daemon = True + timer_thread.start() + + def start_timer(self): + """Start Counting""" + #logger.info("Starting 60-minute timer") + logger.info("Start Counting and Timer pintu-buka 5 minutes") + 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_pintu_buka) + timer_thread.daemon = True + timer_thread.start() + + # Reset detection flags + self.pintu_kiri_buka_detected = False + self.pintu_kanan_buka_detected = False + + def check_timer_expiration_pintu(self): + """Check if timer has expired (5 minutes)""" + time.sleep(5 * 60) # Wait 5 minutes + + if self.timer_active_pintu: + logger.info("Timer Pintu expired (5 minutes)") + self.timer_active_pintu = False + self.pintu_kiri_buka_detected = False + self.pintu_kanan_buka_detected = False + + def check_timer_expiration_pintu_buka(self): + """Check if timer has expired (5 minutes)""" + time.sleep(5 * 60) # Wait 5 minutes + + if self.pintu_buka_timer: + logger.info("Timer Pintu Buka expired (5 minutes)") + self.pintu_buka_timer = False + + def publish_result(self): + """Publish counter result to MQTT topic""" + if self.counter > 0: + topic = f"{self.topic}/{self.camera_name}/karung" + #TESTING + #topic = f"{self.topic}/{self.camera_name}-check_pintu/karung" + message = str(self.counter) + + try: + self.report_client.publish(topic, message) + logger.info(f"Published counter result to {topic}: {message}") + + # Save to database + #self.save_to_database() + + # Save to JSON for temporary persistent storage + #self.save_to_json('frigate_camera') + + except Exception as e: + logger.error(f"Error publishing result: {e}") + else: + logger.info("Counter is zero, not publishing result") + + def save_to_database(self): + """Save counter result to SQLite database""" + try: + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO karung_counts (camera_name, date, counter_value) + VALUES (?, ?, ?) + ''', (self.camera_name, date.today(), self.counter)) + conn.commit() + conn.close() + logger.info(f"Saved counter result to database: {self.counter}") + except Exception as e: + logger.error(f"Error saving to database: {e}") + # Ensure we don't lose data due to database errors + # We should still try to save to JSON as backup + try: + self.save_to_json(self.camera_name) + logger.info("Fallback save to JSON successful") + except Exception as e2: + logger.error(f"Fallback save to JSON also failed: {e2}") + + def save_to_json(self, camera_name): + """Save counter result to JSON file for temporary persistent storage""" + try: + # Read existing data + if os.path.exists(self.json_storage_path): + with open(self.json_storage_path, 'r') as f: + data = json.load(f) + else: + data = {} + + # Update counter value for this camera + if camera_name not in data: + data[camera_name] = {} + data[camera_name]["karung"] = self.counter + + # Write back to file + with open(self.json_storage_path, 'w') as f: + json.dump(data, f, indent=2) + + logger.info(f"Saved counter result to JSON storage for {camera_name}: {self.counter}") + except Exception as e: + logger.error(f"Error saving to JSON storage: {e}") + # Try to create a backup of the existing file before overwriting + try: + import shutil + backup_path = f"{self.json_storage_path}.backup" + if os.path.exists(self.json_storage_path): + shutil.copy2(self.json_storage_path, backup_path) + logger.info(f"Created backup of JSON storage: {backup_path}") + except Exception as backup_e: + logger.error(f"Failed to create backup of JSON storage: {backup_e}") + raise # Re-raise the original exception + + def load_previous_counter(self): + """Load the previous counter value from JSON storage on startup""" + try: + self.counter = self.load_from_json(self.camera_name) + logger.info(f"Loaded previous counter value: {self.counter}") + except Exception as e: + logger.error(f"Error loading previous counter value: {e}") + # If loading fails, start with 0 counter + self.counter = 0 + + def load_from_json(self, camera_name): + """Load counter result from JSON file""" + try: + if os.path.exists(self.json_storage_path): + with open(self.json_storage_path, 'r') as f: + data = json.load(f) + if camera_name in data and "karung" in data[camera_name]: + return data[camera_name]["karung"] + return 0 + except Exception as e: + logger.error(f"Error loading from JSON storage: {e}") + # Return 0 in case of error for safety + return 0 + + def reset_counter(self): + """Reset counter and clear detection flags""" + logger.info("Resetting counter at midnight") + #with self.counter_lock: + # self.counter = 0 + # 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) + self.pintu_kiri_buka_detected = False + self.pintu_kanan_buka_detected = False + self.timer_active = False + self.timer_start_time = None + self.seen_objects = {} + + def run(self): + """Main run loop""" + logger.info("Starting Frigate Counter Service") + + try: + while True: + # Run scheduled tasks + schedule.run_pending() + time.sleep(1) + except KeyboardInterrupt: + logger.info("Shutting down Frigate Counter Service") + self.frigate_client.loop_stop() + self.report_client.loop_stop() + self.frigate_client.disconnect() + self.report_client.disconnect() + +if __name__ == "__main__": + counter_service = FrigateCounter() + counter_service.run() diff --git a/karung_counts.db b/karung_masuk.db similarity index 92% rename from karung_counts.db rename to karung_masuk.db index 3617402c52a4f152e5dc8543cee33b658fd0ddda..0b1216e8dbaeda2e30cc99209f758083cc41c45e 100644 GIT binary patch literal 12288 zcmeI2&u$Vy9LHy%(m#}?p14j=v}rNRF3>JsT-(tQf#P;aBr(~HuqxqCTUdJWX1w$b zdYv@l0EBB7>2O@)@%LzJ$%s}WbMF=rEz z*nC2<`8oSwHEZU~eC7`uP8IujEo5W=tXTp&`J!N5!R2_AgAFS=V+~8ORxD&UR~&2{ zEA&{QWg6+&qYsKbQf%1&)ATPQKL{9r0T_S*7=Qs7fB_hQ0T_S*7=VHMVBjH*XX(Np HAd> delta 65 zcmZojXh@hK&B!=W##xY&L9a`Imw|zSi7$hJZx>(2WE_$M~V Jv4K<|xBvjo58D6$ diff --git a/karung_masuk.json b/karung_masuk.json new file mode 100644 index 0000000..6c542a5 --- /dev/null +++ b/karung_masuk.json @@ -0,0 +1,5 @@ +{ + "kandang_1_karung_masuk": { + "karung": 41 + } +} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..0682e37 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,222 @@ + + + + + + {% block title %}Frigate Counter Karung Masuk{% endblock %} + + {% block extra_css %}{% endblock %} + + +
+
+

Frigate Counter Karung Masuk

+ +
+
+ +
+ {% block content %}{% endblock %} +
+ +
+

Frigate Counter - Real-time bag counting system

+
+ + {% 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..254a766 --- /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=8898, debug=True)