Update frigate_counter to GPIO

This commit is contained in:
2026-04-07 16:08:03 +07:00
parent 0339f178cd
commit c52d608d58
11 changed files with 1194 additions and 41 deletions

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ import threading
import os import os
import logging import logging
import json import json
import requests
from datetime import datetime, date from datetime import datetime, date
from typing import Optional from typing import Optional
@@ -28,25 +29,32 @@ class FrigateCounter:
# MQTT configuration from environment variables # MQTT configuration from environment variables
self.frigate_mqtt_host = os.environ.get('FRIGATE_MQTT_HOST', 'localhost') self.frigate_mqtt_host = os.environ.get('FRIGATE_MQTT_HOST', 'localhost')
self.frigate_mqtt_port = int(os.environ.get('FRIGATE_MQTT_PORT', 1883)) 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.report_mqtt_port = int(os.environ.get('REPORT_MQTT_PORT', 1883))
self.top_topic = os.environ.get("TOP_TOPIC", "cpsp") self.top_topic = os.environ.get("TOP_TOPIC", "cpsp")
self.site_name = os.environ.get('SITE_NAME', 'sukawarna') self.site_name = os.environ.get('SITE_NAME', 'sukawarna')
self.topic = os.environ.get('TOPIC', f"{self.top_topic}/counter/{self.site_name}") 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.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"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"REPORT_MQTT_HOST: {self.report_mqtt_host}:{self.report_mqtt_port}")
logger.info(f"TOPIC: {self.topic}") logger.info(f"TOPIC: {self.topic}")
logger.info(f"CAMERA_NAME: {self.camera_name}") 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 # Database setup
self.db_path = 'karung_counts.db' self.db_path = '/etc/frigate-counter/karung-masuk/karung_masuk.db'
self.init_database() self.init_database()
# JSON storage for temporary persistent counter values # 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() self.init_json_storage()
# State tracking # State tracking
@@ -62,6 +70,8 @@ class FrigateCounter:
self.timer_active_pintu = False self.timer_active_pintu = False
self.timer_start_time_pintu = None self.timer_start_time_pintu = None
self.pintu_buka_timer = False
# Load previous counter value on startup # Load previous counter value on startup
self.load_previous_counter() self.load_previous_counter()
@@ -75,6 +85,20 @@ class FrigateCounter:
# Schedule daily reset at midnight # Schedule daily reset at midnight
schedule.every().day.at("23:59").do(self.reset_counter) 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): def init_database(self):
"""Initialize SQLite database with required table""" """Initialize SQLite database with required table"""
conn = sqlite3.connect(self.db_path) conn = sqlite3.connect(self.db_path)
@@ -154,7 +178,8 @@ class FrigateCounter:
# Dont detect stationary # Dont detect stationary
stationary = event_after.get("stationary") stationary = event_after.get("stationary")
if stationary: #if stationary and:
if stationary and label == "karung":
return return
new_zones = [z for z in zones_after if z not in zones_before] 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) self.handle_pintu_kanan_buka(camera_name)
elif label == "karung" and self.timer_active: elif label == "karung" and self.timer_active:
self.handle_karung(camera_name, track_id) 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) self.handle_pintu_tutup(camera_name)
except Exception as e: except Exception as e:
@@ -194,6 +219,9 @@ class FrigateCounter:
self.timer_active = False self.timer_active = False
self.seen_objects = {} self.seen_objects = {}
# Call RPi
self.action_relay_off()
def handle_pintu_kiri_buka(self, camera_name): def handle_pintu_kiri_buka(self, camera_name):
"""Handle detection of pintu-kiri-buka""" """Handle detection of pintu-kiri-buka"""
logger.info(f"Detected pintu-kiri-buka on {camera_name}") logger.info(f"Detected pintu-kiri-buka on {camera_name}")
@@ -254,19 +282,23 @@ class FrigateCounter:
def start_timer(self): def start_timer(self):
"""Start Counting""" """Start Counting"""
#logger.info("Starting 60-minute timer") #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_active = True
self.timer_start_time = datetime.now() self.timer_start_time = datetime.now()
self.pintu_buka_timer = True
# Schedule timer expiration check # Schedule timer expiration check
#timer_thread = threading.Thread(target=self.check_timer_expiration) timer_thread = threading.Thread(target=self.check_timer_expiration_pintu_buka)
#timer_thread.daemon = True timer_thread.daemon = True
#timer_thread.start() timer_thread.start()
# Reset detection flags # Reset detection flags
self.pintu_kiri_buka_detected = False self.pintu_kiri_buka_detected = False
self.pintu_kanan_buka_detected = False self.pintu_kanan_buka_detected = False
# Call to RPI
self.action_relay_on()
def check_timer_expiration_pintu(self): def check_timer_expiration_pintu(self):
"""Check if timer has expired (5 minutes)""" """Check if timer has expired (5 minutes)"""
time.sleep(5 * 60) # Wait 5 minutes time.sleep(5 * 60) # Wait 5 minutes
@@ -277,22 +309,28 @@ class FrigateCounter:
self.pintu_kiri_buka_detected = False self.pintu_kiri_buka_detected = False
self.pintu_kanan_buka_detected = False self.pintu_kanan_buka_detected = False
def check_timer_expiration(self): # Call RPi
"""Check if timer has expired (60 minutes)""" #if not self.timer_active:
time.sleep(60 * 60) # Wait 60 minutes # self.action_relay_off()
if self.timer_active: def check_timer_expiration_pintu_buka(self):
logger.info("Timer expired (60 minutes)") """Check if timer has expired (5 minutes)"""
self.timer_active = False time.sleep(5 * 60) # Wait 5 minutes
#self.publish_result()
#self.reset_counter() 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): def publish_result(self):
"""Publish counter result to MQTT topic""" """Publish counter result to MQTT topic"""
if self.counter > 0: if self.counter > 0:
topic = f"{self.topic}/{self.camera_name}/karung" topic = f"{self.topic}/{self.camera_name}/karung"
#TESTING #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) message = str(self.counter)
try: try:
@@ -397,6 +435,7 @@ class FrigateCounter:
# self.save_to_database() # self.save_to_database()
# self.save_to_json(self.camera_name) # self.save_to_json(self.camera_name)
# Save current counter value before resetting # Save current counter value before resetting
self.publish_result()
self.save_to_database() self.save_to_database()
self.counter = 0 self.counter = 0
self.save_to_json(self.camera_name) self.save_to_json(self.camera_name)

433
frigate_counter.py.backup Normal file
View File

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

Binary file not shown.

5
karung_masuk.json Normal file
View File

@@ -0,0 +1,5 @@
{
"kandang_1_karung_masuk": {
"karung": 41
}
}

222
templates/base.html Normal file
View File

@@ -0,0 +1,222 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Frigate Counter Karung Masuk{% endblock %}</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: #f5f5f5;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: #2c3e50;
color: white;
padding: 20px 0;
margin-bottom: 30px;
}
header h1 {
text-align: center;
font-size: 1.8rem;
}
header nav {
text-align: center;
margin-top: 10px;
}
header nav a {
color: white;
text-decoration: none;
margin: 0 15px;
padding: 5px 10px;
border-radius: 4px;
transition: background 0.3s;
}
header nav a:hover {
background: rgba(255,255,255,0.2);
}
.card {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.card h2 {
margin-bottom: 15px;
color: #2c3e50;
border-bottom: 2px solid #3498db;
padding-bottom: 10px;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 10px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #3498db;
color: white;
font-weight: 600;
}
tr:hover {
background: #f8f9fa;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.85em;
font-weight: 600;
}
.badge-primary {
background: #3498db;
color: white;
}
.badge-success {
background: #27ae60;
color: white;
}
.badge-info {
background: #17a2b8;
color: white;
}
a {
color: #3498db;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.stat-card {
background: white;
border-radius: 8px;
padding: 20px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.stat-card h3 {
font-size: 2rem;
color: #3498db;
margin-bottom: 5px;
}
.stat-card p {
color: #666;
font-size: 0.9rem;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
}
.empty-state h3 {
margin-bottom: 10px;
color: #999;
}
.date-selector {
margin-bottom: 20px;
}
.date-selector select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.btn {
display: inline-block;
padding: 8px 16px;
border-radius: 4px;
text-decoration: none;
transition: background 0.3s;
font-size: 0.9rem;
}
.btn-primary {
background: #3498db;
color: white;
}
.btn-primary:hover {
background: #2980b9;
text-decoration: none;
}
footer {
text-align: center;
padding: 20px;
color: #666;
margin-top: 40px;
}
</style>
{% block extra_css %}{% endblock %}
</head>
<body>
<header>
<div class="container">
<h1>Frigate Counter Karung Masuk</h1>
<nav>
<a href="{{ url_for('index') }}">Dashboard</a>
</nav>
</div>
</header>
<div class="container">
{% block content %}{% endblock %}
</div>
<footer>
<p>Frigate Counter - Real-time bag counting system</p>
</footer>
{% block extra_js %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,52 @@
{% extends "base.html" %}
{% block title %}{{ camera_name }} - Camera Details{% endblock %}
{% block content %}
<div class="card">
<h2>Camera: {{ camera_name }}</h2>
<div class="stats-grid">
<div class="stat-card">
<h3>{{ counts|length }}</h3>
<p>Days with Data</p>
</div>
<div class="stat-card">
<h3>{{ total }}</h3>
<p>Total Counts</p>
</div>
<div class="stat-card">
<h3>{{ (total / counts|length)|round(1) if counts|length > 0 else 0 }}</h3>
<p>Average per Day</p>
</div>
</div>
{% if counts %}
<table>
<thead>
<tr>
<th>Date</th>
<th>Count</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
{% for item in counts %}
<tr>
<td><a href="{{ url_for('date_detail', date_str=item.date) }}">{{ item.date }}</a></td>
<td><span class="badge badge-success">{{ item.counter_value }}</span></td>
<td>{{ item.timestamp }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<h3>No data for this camera</h3>
<p>There are no records for camera {{ camera_name }}.</p>
</div>
{% endif %}
<a href="{{ url_for('index') }}" class="btn btn-primary">← Back to Dashboard</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,58 @@
{% extends "base.html" %}
{% block title %}{{ date }} - Daily Details{% endblock %}
{% block content %}
<div class="card">
<h2>Details for {{ date }}</h2>
<div class="date-selector">
<label for="date-select">Jump to date: </label>
<select id="date-select" onchange="window.location.href=this.value">
<option value="">Select a date...</option>
{% for d in available_dates %}
<option value="{{ url_for('date_detail', date_str=d) }}" {% if d == date %}selected{% endif %}>{{ d }}</option>
{% endfor %}
</select>
</div>
<div class="stats-grid">
<div class="stat-card">
<h3>{{ counts|length }}</h3>
<p>Camera Entries</p>
</div>
<div class="stat-card">
<h3>{{ total }}</h3>
<p>Total Counts</p>
</div>
</div>
{% if counts %}
<table>
<thead>
<tr>
<th>Camera Name</th>
<th>Count</th>
<th>Timestamp</th>
</tr>
</thead>
<tbody>
{% for item in counts %}
<tr>
<td><a href="{{ url_for('camera_detail', camera_name=item.camera_name) }}">{{ item.camera_name }}</a></td>
<td><span class="badge badge-success">{{ item.counter_value }}</span></td>
<td>{{ item.timestamp }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<h3>No data for this date</h3>
<p>There are no records for {{ date }}.</p>
</div>
{% endif %}
<a href="{{ url_for('index') }}" class="btn btn-primary">← Back to Dashboard</a>
</div>
{% endblock %}

123
templates/index.html Normal file
View File

@@ -0,0 +1,123 @@
{% extends "base.html" %}
{% block title %}Dashboard - Frigate Counter{% endblock %}
{% block content %}
<!-- Today's Stats -->
<div class="stats-grid">
<div class="stat-card">
<h3>{{ today_data|length }}</h3>
<p>Cameras Active Today</p>
</div>
<div class="stat-card">
<h3>{{ today_data|sum(attribute='counter_value') }}</h3>
<p>Total Counts Today</p>
</div>
<div class="stat-card">
<h3>{{ summary|length }}</h3>
<p>Days with Data</p>
</div>
<div class="stat-card">
<h3>{{ cameras|sum(attribute='total_count') }}</h3>
<p>All Time Total</p>
</div>
</div>
<!-- Today's Details -->
<div class="card">
<h2>Today's Activity ({{ today }})</h2>
{% if today_data %}
<table>
<thead>
<tr>
<th>Camera</th>
<th>Count</th>
<th>Last Updated</th>
</tr>
</thead>
<tbody>
{% for item in today_data %}
<tr>
<td><a href="{{ url_for('camera_detail', camera_name=item.camera_name) }}">{{ item.camera_name }}</a></td>
<td><span class="badge badge-success">{{ item.counter_value }}</span></td>
<td>{{ item.timestamp }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<h3>No data for today yet</h3>
<p>Counts will appear here once the counter starts receiving events.</p>
</div>
{% endif %}
</div>
<!-- Daily Summary -->
<div class="card">
<h2>Daily Summary</h2>
{% if summary %}
<table>
<thead>
<tr>
<th>Date</th>
<th>Cameras</th>
<th>Total Counts</th>
<th>Last Update</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for day in summary %}
<tr>
<td><strong>{{ day.date }}</strong></td>
<td><span class="badge badge-info">{{ day.camera_count }}</span></td>
<td><span class="badge badge-success">{{ day.total_count }}</span></td>
<td>{{ day.last_update }}</td>
<td><a href="{{ url_for('date_detail', date_str=day.date) }}" class="btn btn-primary">View Details</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<h3>No data available</h3>
<p>Daily summaries will appear here once data is recorded.</p>
</div>
{% endif %}
</div>
<!-- Camera Summary -->
<div class="card">
<h2>Camera Summary</h2>
{% if cameras %}
<table>
<thead>
<tr>
<th>Camera Name</th>
<th>Days Active</th>
<th>Total Counts</th>
<th>Last Update</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for camera in cameras %}
<tr>
<td><strong>{{ camera.camera_name }}</strong></td>
<td><span class="badge badge-info">{{ camera.day_count }}</span></td>
<td><span class="badge badge-success">{{ camera.total_count }}</span></td>
<td>{{ camera.last_update }}</td>
<td><a href="{{ url_for('camera_detail', camera_name=camera.camera_name) }}" class="btn btn-primary">View Details</a></td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="empty-state">
<h3>No cameras registered</h3>
<p>Camera data will appear here once events are processed.</p>
</div>
{% endif %}
</div>
{% endblock %}

221
web_app.py Normal file
View File

@@ -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/<date_str>')
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/<camera_name>')
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)