Compare commits
24
Commits
d7e6d52141
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29490b8da2 | ||
|
|
c42c10285a | ||
|
|
39222d2e4a | ||
|
|
f6f8738a4b | ||
|
|
6e8dec689c | ||
|
|
c52d608d58 | ||
|
|
e8bf41ab6b | ||
|
|
7990c423cb | ||
|
|
3177c40957 | ||
|
|
2d0c41f426 | ||
|
|
ebb1622314 | ||
|
|
5023a583a6 | ||
|
|
14005331ac | ||
|
|
078c91283d | ||
|
|
0339f178cd | ||
|
|
048f66919a | ||
|
|
f02498bdfd | ||
|
|
665c365b5b | ||
|
|
b0ce911be3 | ||
|
|
0337d35b42 | ||
|
|
8232989bd5 | ||
|
|
dfc04cfcff | ||
|
|
a42278aa39 | ||
|
|
f760737716 |
No files matched your search
@@ -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
|
|
||||||
@@ -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
|
||||||
+343
-62
@@ -13,34 +13,66 @@ 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, Dict, Any, List
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
||||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FrigateCounter:
|
class FrigateCounter:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# 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_port = int(os.environ.get('REPORT_MQTT_PORT', 1883))
|
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.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(
|
||||||
self.camera_name = os.environ.get('CAMERA_NAME', 'kandang_1_karung_masuk_test')
|
"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
|
# Database setup
|
||||||
self.db_path = 'karung_counts.db'
|
self.db_path = "/etc/frigate-counter/karung-masuk/karung_masuk.db"
|
||||||
|
#self.db_path = "/tmp/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.json_storage_path = "/tmp/karung_masuk.json"
|
||||||
self.init_json_storage()
|
self.init_json_storage()
|
||||||
|
|
||||||
# State tracking
|
# State tracking
|
||||||
@@ -48,10 +80,24 @@ class FrigateCounter:
|
|||||||
self.pintu_kanan_buka_detected = False
|
self.pintu_kanan_buka_detected = False
|
||||||
self.timer_active = False
|
self.timer_active = False
|
||||||
self.timer_start_time = None
|
self.timer_start_time = None
|
||||||
|
|
||||||
|
# Counter IN/DOWN
|
||||||
self.counter = 0
|
self.counter = 0
|
||||||
self.counter_lock = threading.Lock()
|
self.counter_lock = threading.Lock()
|
||||||
|
|
||||||
|
# Counter OUT/UP
|
||||||
|
self.counter_out = 0
|
||||||
|
|
||||||
self.seen_objects = {}
|
self.seen_objects = {}
|
||||||
|
|
||||||
|
# State pintu tracking
|
||||||
|
self.timer_active_pintu = False
|
||||||
|
self.timer_start_time_pintu = None
|
||||||
|
self.timer_active_pintu_tutup = False
|
||||||
|
self.timer_start_time_pintu_tutup = 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()
|
||||||
|
|
||||||
@@ -62,14 +108,80 @@ class FrigateCounter:
|
|||||||
# Initialize MQTT clients
|
# Initialize MQTT clients
|
||||||
self.setup_mqtt_clients()
|
self.setup_mqtt_clients()
|
||||||
|
|
||||||
|
# Direction detection
|
||||||
|
self.previous_positions: Dict[str, float] = {}
|
||||||
|
# Movement threshold (pixels)
|
||||||
|
self.movement_threshold = float(os.environ.get("MOVEMENT_THRESHOLD", "2.0"))
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
|
# ==================== START - DIRECTION ====================
|
||||||
|
def _get_object_key(self, camera: str, label: str, obj_id: str) -> str:
|
||||||
|
return f"{camera}:{label}:{obj_id}"
|
||||||
|
|
||||||
|
def _get_box_center_y(self, box: list) -> float:
|
||||||
|
"""Calculate center Y of bounding box [x_min, y_min, x_max, y_max]."""
|
||||||
|
if not box or len(box) != 4:
|
||||||
|
return 0.0
|
||||||
|
return (box[1] + box[3]) / 2.0
|
||||||
|
|
||||||
|
def calculate_direction(
|
||||||
|
self, camera: str, obj_id: str, current_box: list
|
||||||
|
) -> Optional[str]:
|
||||||
|
key = self._get_object_key(camera, "karung", obj_id)
|
||||||
|
current_y = self._get_box_center_y(current_box)
|
||||||
|
|
||||||
|
if current_y == 0.0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if key not in self.previous_positions:
|
||||||
|
self.previous_positions[key] = current_y
|
||||||
|
return None
|
||||||
|
|
||||||
|
previous_y = self.previous_positions[key]
|
||||||
|
self.previous_positions[key] = current_y
|
||||||
|
|
||||||
|
delta_y = current_y - previous_y
|
||||||
|
|
||||||
|
if abs(delta_y) < self.movement_threshold:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Y increases downward in image coordinates
|
||||||
|
if delta_y < 0:
|
||||||
|
return "UP"
|
||||||
|
else:
|
||||||
|
return "DOWN"
|
||||||
|
|
||||||
|
def remove_object(self, camera: str, label: str, obj_id: str):
|
||||||
|
key = self._get_object_key(camera, label, obj_id)
|
||||||
|
if key in self.previous_positions:
|
||||||
|
del self.previous_positions[key]
|
||||||
|
|
||||||
|
# ==================== END - DIRECTION ====================
|
||||||
|
|
||||||
|
# ==================== START - RELAY LAMPU ====================
|
||||||
|
def action_relay_on(self):
|
||||||
|
try:
|
||||||
|
requests.get(self.relay_on, timeout=1)
|
||||||
|
logger.info("Relay ON")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
def action_relay_off(self):
|
||||||
|
try:
|
||||||
|
requests.get(self.relay_off, timeout=2)
|
||||||
|
logger.info("Relay OFF")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
# ==================== END - RELAY LAMPU ====================
|
||||||
|
#
|
||||||
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)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS karung_counts (
|
CREATE TABLE IF NOT EXISTS karung_counts (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
camera_name TEXT NOT NULL,
|
camera_name TEXT NOT NULL,
|
||||||
@@ -77,7 +189,7 @@ class FrigateCounter:
|
|||||||
counter_value INTEGER NOT NULL,
|
counter_value INTEGER NOT NULL,
|
||||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)
|
)
|
||||||
''')
|
""")
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
logger.info("Database initialized")
|
logger.info("Database initialized")
|
||||||
@@ -86,7 +198,7 @@ class FrigateCounter:
|
|||||||
"""Initialize JSON storage file for temporary persistent counter values"""
|
"""Initialize JSON storage file for temporary persistent counter values"""
|
||||||
if not os.path.exists(self.json_storage_path):
|
if not os.path.exists(self.json_storage_path):
|
||||||
# Create empty JSON file with empty dictionary
|
# Create empty JSON file with empty dictionary
|
||||||
with open(self.json_storage_path, 'w') as f:
|
with open(self.json_storage_path, "w") as f:
|
||||||
json.dump({}, f)
|
json.dump({}, f)
|
||||||
logger.info("JSON storage initialized")
|
logger.info("JSON storage initialized")
|
||||||
|
|
||||||
@@ -120,18 +232,22 @@ class FrigateCounter:
|
|||||||
"""Handle incoming Frigate MQTT messages"""
|
"""Handle incoming Frigate MQTT messages"""
|
||||||
try:
|
try:
|
||||||
# Parse the message (assuming JSON format)
|
# Parse the message (assuming JSON format)
|
||||||
#import json
|
# import json
|
||||||
payload = json.loads(msg.payload.decode())
|
payload = json.loads(msg.payload.decode())
|
||||||
|
|
||||||
if "after" not in payload:
|
if "after" not in payload:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# event_after = payload["after"]
|
||||||
event_after = payload["after"]
|
event_after = payload.get("after", {})
|
||||||
event_before = payload.get("before", {})
|
event_before = payload.get("before", {})
|
||||||
|
|
||||||
camera_name = event_after.get("camera", "unknown")
|
camera_name = event_after.get("camera", "unknown")
|
||||||
event_type = event_after.get('type', '')
|
|
||||||
|
if camera_name != self.camera_name:
|
||||||
|
return
|
||||||
|
|
||||||
|
event_type = event_after.get("type", "")
|
||||||
label = event_after.get("label", "")
|
label = event_after.get("label", "")
|
||||||
track_id = event_after.get("id")
|
track_id = event_after.get("id")
|
||||||
zones_after = event_after.get("entered_zones", [])
|
zones_after = event_after.get("entered_zones", [])
|
||||||
@@ -139,7 +255,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]
|
||||||
@@ -151,23 +268,68 @@ class FrigateCounter:
|
|||||||
return # sudah dihitung
|
return # sudah dihitung
|
||||||
|
|
||||||
# Extract object type and camera name
|
# Extract object type and camera name
|
||||||
#camera_name = data.get('camera', 'unknown')
|
# camera_name = data.get('camera', 'unknown')
|
||||||
#event_type = data.get('type', '')
|
# event_type = data.get('type', '')
|
||||||
#label = data.get('label', '')
|
# label = data.get('label', '')
|
||||||
|
|
||||||
logger.debug(f"Received message: camera={camera_name}, type={event_type}, label={label}")
|
logger.debug(
|
||||||
|
f"Received message: camera={camera_name}, type={event_type}, label={label}, zones_after={zones_after}, zones_before={zones_before}, timer_active={self.timer_active}, pintu_buka_timer={self.pintu_buka_timer}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use after data primarily, fallback to before
|
||||||
|
data = event_after if event_after else event_before
|
||||||
|
|
||||||
# Handle different object types
|
# Handle different object types
|
||||||
if label == "pintu-kiri-buka" and not self.timer_active:
|
if (
|
||||||
|
label == "pintu-kiri-buka"
|
||||||
|
and not self.timer_active
|
||||||
|
and not self.pintu_kiri_buka_detected
|
||||||
|
and not self.timer_active_pintu_tutup
|
||||||
|
):
|
||||||
self.handle_pintu_kiri_buka(camera_name)
|
self.handle_pintu_kiri_buka(camera_name)
|
||||||
elif label == "pintu-kanan-buka" and not self.timer_active:
|
elif (
|
||||||
|
label == "pintu-kanan-buka"
|
||||||
|
and not self.timer_active
|
||||||
|
and not self.pintu_kanan_buka_detected
|
||||||
|
and not self.timer_active_pintu_tutup
|
||||||
|
):
|
||||||
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)
|
if event_type == "end":
|
||||||
|
self.remove_object(camera_name, "karung", track_id)
|
||||||
|
logger.info(
|
||||||
|
f"[END] Object ended - Camera: {camera}, Label: {label}, ID: {obj_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
box = data.get("box")
|
||||||
|
self.handle_karung(camera_name, track_id, box)
|
||||||
|
|
||||||
|
# elif label == "pintu-tutup" and self.timer_active and self.pintu_tutup_zone_name in zones_after and not self.pintu_buka_timer:
|
||||||
|
elif (
|
||||||
|
label == "pintu-tutup"
|
||||||
|
and self.timer_active
|
||||||
|
and not self.pintu_buka_timer
|
||||||
|
):
|
||||||
|
self.handle_pintu_tutup(camera_name)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing MQTT message: {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 = {}
|
||||||
|
|
||||||
|
if not self.timer_active_pintu_tutup:
|
||||||
|
self.start_timer_pintu_tutup()
|
||||||
|
|
||||||
|
# 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}")
|
||||||
@@ -190,18 +352,30 @@ class FrigateCounter:
|
|||||||
else:
|
else:
|
||||||
logger.debug("Ignoring pintu-kanan-buka during timer period")
|
logger.debug("Ignoring pintu-kanan-buka during timer period")
|
||||||
|
|
||||||
def handle_karung(self, camera_name, track_id):
|
def handle_karung(self, camera_name, track_id, box):
|
||||||
"""Handle detection of karung object"""
|
"""Handle detection of karung object"""
|
||||||
logger.info(f"Detected karung on {camera_name}")
|
logger.info(f"Detected karung on {camera_name}")
|
||||||
|
|
||||||
|
# ======================= CALCULATE DIRECTION ==========================
|
||||||
|
direction = None
|
||||||
|
if box:
|
||||||
|
direction = self.calculate_direction(camera_name, track_id, box)
|
||||||
|
|
||||||
# Only count if timer is active
|
# Only count if timer is active
|
||||||
if self.timer_active:
|
if self.timer_active:
|
||||||
with self.counter_lock:
|
with self.counter_lock:
|
||||||
self.counter += 1
|
if direction:
|
||||||
self.seen_objects[camera_name][track_id] = datetime.now()
|
if direction == "DOWN":
|
||||||
self.save_to_json(camera_name)
|
self.counter += 1
|
||||||
self.publish_result()
|
elif direction == "UP":
|
||||||
logger.info(f"Counter incremented to {self.counter} and republish to MQTT")
|
self.counter_out += 1
|
||||||
|
|
||||||
|
self.seen_objects[camera_name][track_id] = datetime.now()
|
||||||
|
self.save_to_json(camera_name)
|
||||||
|
self.publish_result()
|
||||||
|
logger.info(
|
||||||
|
f"Direction={direction}, Counter_IN={self.counter}, Counter_OUT={self.counter_out} and republish to MQTT"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
logger.debug("Ignoring karung outside timer period")
|
logger.debug("Ignoring karung outside timer period")
|
||||||
|
|
||||||
@@ -209,15 +383,49 @@ class FrigateCounter:
|
|||||||
"""Check if both pintu-kiri-buka and pintu-kanan-buka have been detected"""
|
"""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:
|
if self.pintu_kiri_buka_detected and self.pintu_kanan_buka_detected:
|
||||||
self.start_timer()
|
self.start_timer()
|
||||||
|
return
|
||||||
|
|
||||||
def start_timer(self):
|
if not self.timer_active_pintu:
|
||||||
"""Start the 30-minute timer"""
|
self.start_timer_pintu()
|
||||||
logger.info("Starting 30-minute timer")
|
|
||||||
self.timer_active = True
|
def start_timer_pintu_tutup(self):
|
||||||
self.timer_start_time = datetime.now()
|
"""Start the 30-seconds timer Pintu"""
|
||||||
|
logger.info("Starting 30-seconds timer Pintu Tutup")
|
||||||
|
self.timer_active_pintu_tutup = True
|
||||||
|
self.timer_start_time_pintu_tutup = datetime.now()
|
||||||
|
|
||||||
# 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_tutup)
|
||||||
|
timer_thread.daemon = True
|
||||||
|
timer_thread.start()
|
||||||
|
|
||||||
|
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")
|
||||||
|
# logger.info("Start Counting, Timer Counting 30-minutes 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_counting)
|
||||||
|
# timer_thread.daemon = True
|
||||||
|
# timer_thread.start()
|
||||||
|
|
||||||
|
# Schedule timer expiration check
|
||||||
|
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()
|
||||||
|
|
||||||
@@ -225,33 +433,76 @@ 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 to RPI
|
||||||
|
self.action_relay_on()
|
||||||
|
|
||||||
|
def check_timer_expiration_counting(self):
|
||||||
"""Check if timer has expired (30 minutes)"""
|
"""Check if timer has expired (30 minutes)"""
|
||||||
time.sleep(30 * 60) # Wait 30 minutes
|
time.sleep(30 * 60) # Wait 5 minutes
|
||||||
|
|
||||||
if self.timer_active:
|
if self.timer_active:
|
||||||
logger.info("Timer expired (30 minutes)")
|
logger.info("Stop Counting. Timer Counting Expired (30 minutes)")
|
||||||
self.timer_active = False
|
self.timer_active = False
|
||||||
#self.publish_result()
|
self.pintu_kiri_buka_detected = False
|
||||||
#self.reset_counter()
|
self.pintu_kanan_buka_detected = False
|
||||||
|
self.seen_objects = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Call RPi
|
||||||
|
# if not self.timer_active:
|
||||||
|
# self.action_relay_off()
|
||||||
|
|
||||||
|
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 check_timer_expiration_pintu_tutup(self):
|
||||||
|
"""Check if timer has expired (30 seconds)"""
|
||||||
|
time.sleep(30) # Wait 5 minutes
|
||||||
|
|
||||||
|
if self.timer_active_pintu_tutup:
|
||||||
|
logger.info("Timer Pintu Tutup expired (30 seconds)")
|
||||||
|
self.timer_active_pintu_tutup = False
|
||||||
|
|
||||||
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
|
topic_out = f"{self.topic}/{self.camera_name}_out/karung"
|
||||||
topic = f"{self.topic}/{self.camera_name}-check_pintu/karung"
|
# TESTING
|
||||||
|
# topic = f"{self.topic}/{self.camera_name}-check_pintu/karung"
|
||||||
message = str(self.counter)
|
message = str(self.counter)
|
||||||
|
message_out = str(self.counter_out)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.report_client.publish(topic, message)
|
self.report_client.publish(topic, message)
|
||||||
logger.info(f"Published counter result to {topic}: {message}")
|
logger.info(f"Published counter result to {topic}: {message}")
|
||||||
|
|
||||||
|
self.report_client.publish(topic_out, message_out)
|
||||||
|
logger.info(f"Published counter result to {topic_out}: {message_out}")
|
||||||
|
|
||||||
# Save to database
|
# Save to database
|
||||||
#self.save_to_database()
|
# self.save_to_database()
|
||||||
|
|
||||||
# Save to JSON for temporary persistent storage
|
# Save to JSON for temporary persistent storage
|
||||||
#self.save_to_json('frigate_camera')
|
# self.save_to_json('frigate_camera')
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error publishing result: {e}")
|
logger.error(f"Error publishing result: {e}")
|
||||||
@@ -263,10 +514,22 @@ class FrigateCounter:
|
|||||||
try:
|
try:
|
||||||
conn = sqlite3.connect(self.db_path)
|
conn = sqlite3.connect(self.db_path)
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute('''
|
cursor.execute(
|
||||||
|
"""
|
||||||
INSERT INTO karung_counts (camera_name, date, counter_value)
|
INSERT INTO karung_counts (camera_name, date, counter_value)
|
||||||
VALUES (?, ?, ?)
|
VALUES (?, ?, ?)
|
||||||
''', ('frigate_camera', date.today(), self.counter))
|
""",
|
||||||
|
(self.camera_name, date.today(), self.counter),
|
||||||
|
)
|
||||||
|
|
||||||
|
camera_name_out = f"{self.camera_name}_out"
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO karung_counts (camera_name, date, counter_value)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(camera_name_out, date.today(), self.counter_out),
|
||||||
|
)
|
||||||
conn.commit()
|
conn.commit()
|
||||||
conn.close()
|
conn.close()
|
||||||
logger.info(f"Saved counter result to database: {self.counter}")
|
logger.info(f"Saved counter result to database: {self.counter}")
|
||||||
@@ -275,7 +538,7 @@ class FrigateCounter:
|
|||||||
# Ensure we don't lose data due to database errors
|
# Ensure we don't lose data due to database errors
|
||||||
# We should still try to save to JSON as backup
|
# We should still try to save to JSON as backup
|
||||||
try:
|
try:
|
||||||
self.save_to_json('frigate_camera')
|
self.save_to_json(self.camera_name)
|
||||||
logger.info("Fallback save to JSON successful")
|
logger.info("Fallback save to JSON successful")
|
||||||
except Exception as e2:
|
except Exception as e2:
|
||||||
logger.error(f"Fallback save to JSON also failed: {e2}")
|
logger.error(f"Fallback save to JSON also failed: {e2}")
|
||||||
@@ -285,7 +548,7 @@ class FrigateCounter:
|
|||||||
try:
|
try:
|
||||||
# Read existing data
|
# Read existing data
|
||||||
if os.path.exists(self.json_storage_path):
|
if os.path.exists(self.json_storage_path):
|
||||||
with open(self.json_storage_path, 'r') as f:
|
with open(self.json_storage_path, "r") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
else:
|
else:
|
||||||
data = {}
|
data = {}
|
||||||
@@ -295,16 +558,24 @@ class FrigateCounter:
|
|||||||
data[camera_name] = {}
|
data[camera_name] = {}
|
||||||
data[camera_name]["karung"] = self.counter
|
data[camera_name]["karung"] = self.counter
|
||||||
|
|
||||||
|
# COunter Out
|
||||||
|
camera_name_out = f"{camera_name}_out"
|
||||||
|
if camera_name_out not in data:
|
||||||
|
data[camera_name_out] = {}
|
||||||
|
|
||||||
# Write back to file
|
# Write back to file
|
||||||
with open(self.json_storage_path, 'w') as f:
|
with open(self.json_storage_path, "w") as f:
|
||||||
json.dump(data, f, indent=2)
|
json.dump(data, f, indent=2)
|
||||||
|
|
||||||
logger.info(f"Saved counter result to JSON storage for {camera_name}: {self.counter}")
|
logger.info(
|
||||||
|
f"Saved counter result to JSON storage for {camera_name}: {self.counter}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error saving to JSON storage: {e}")
|
logger.error(f"Error saving to JSON storage: {e}")
|
||||||
# Try to create a backup of the existing file before overwriting
|
# Try to create a backup of the existing file before overwriting
|
||||||
try:
|
try:
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
backup_path = f"{self.json_storage_path}.backup"
|
backup_path = f"{self.json_storage_path}.backup"
|
||||||
if os.path.exists(self.json_storage_path):
|
if os.path.exists(self.json_storage_path):
|
||||||
shutil.copy2(self.json_storage_path, backup_path)
|
shutil.copy2(self.json_storage_path, backup_path)
|
||||||
@@ -317,7 +588,10 @@ class FrigateCounter:
|
|||||||
"""Load the previous counter value from JSON storage on startup"""
|
"""Load the previous counter value from JSON storage on startup"""
|
||||||
try:
|
try:
|
||||||
self.counter = self.load_from_json(self.camera_name)
|
self.counter = self.load_from_json(self.camera_name)
|
||||||
logger.info(f"Loaded previous counter value: {self.counter}")
|
self.counter_out = self.load_from_json(f"{self.camera_name}_out")
|
||||||
|
logger.info(
|
||||||
|
f"Loaded previous counter_IN: {self.counter}, counter_out: {self.counter_out}"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error loading previous counter value: {e}")
|
logger.error(f"Error loading previous counter value: {e}")
|
||||||
# If loading fails, start with 0 counter
|
# If loading fails, start with 0 counter
|
||||||
@@ -327,7 +601,7 @@ class FrigateCounter:
|
|||||||
"""Load counter result from JSON file"""
|
"""Load counter result from JSON file"""
|
||||||
try:
|
try:
|
||||||
if os.path.exists(self.json_storage_path):
|
if os.path.exists(self.json_storage_path):
|
||||||
with open(self.json_storage_path, 'r') as f:
|
with open(self.json_storage_path, "r") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
if camera_name in data and "karung" in data[camera_name]:
|
if camera_name in data and "karung" in data[camera_name]:
|
||||||
return data[camera_name]["karung"]
|
return data[camera_name]["karung"]
|
||||||
@@ -340,16 +614,22 @@ class FrigateCounter:
|
|||||||
def reset_counter(self):
|
def reset_counter(self):
|
||||||
"""Reset counter and clear detection flags"""
|
"""Reset counter and clear detection flags"""
|
||||||
logger.info("Resetting counter at midnight")
|
logger.info("Resetting counter at midnight")
|
||||||
with self.counter_lock:
|
# with self.counter_lock:
|
||||||
# Save current counter value before resetting
|
# self.counter = 0
|
||||||
self.save_to_database()
|
# self.save_to_database()
|
||||||
self.save_to_json('frigate_camera')
|
# self.save_to_json(self.camera_name)
|
||||||
self.counter = 0
|
# Save current counter value before resetting
|
||||||
|
self.publish_result()
|
||||||
|
self.save_to_database()
|
||||||
|
self.counter = 0
|
||||||
|
self.counter_out = 0
|
||||||
|
self.save_to_json(self.camera_name)
|
||||||
self.pintu_kiri_buka_detected = False
|
self.pintu_kiri_buka_detected = False
|
||||||
self.pintu_kanan_buka_detected = False
|
self.pintu_kanan_buka_detected = False
|
||||||
self.timer_active = False
|
self.timer_active = False
|
||||||
self.timer_start_time = None
|
self.timer_start_time = None
|
||||||
self.seen_objects = {}
|
self.seen_objects = {}
|
||||||
|
self.previous_positions = {}
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Main run loop"""
|
"""Main run loop"""
|
||||||
@@ -367,6 +647,7 @@ class FrigateCounter:
|
|||||||
self.frigate_client.disconnect()
|
self.frigate_client.disconnect()
|
||||||
self.report_client.disconnect()
|
self.report_client.disconnect()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
counter_service = FrigateCounter()
|
counter_service = FrigateCounter()
|
||||||
counter_service.run()
|
counter_service.run()
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
#!/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
|
||||||
|
import requests
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# 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 = '/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 action_relay_on(self):
|
||||||
|
try:
|
||||||
|
requests.get(self.relay_on, timeout=1)
|
||||||
|
logger.info("Relay ON")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
def action_relay_off(self):
|
||||||
|
try:
|
||||||
|
requests.get(self.relay_off, timeout=2)
|
||||||
|
logger.info("Relay OFF")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
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 and:
|
||||||
|
if stationary and label == "karung":
|
||||||
|
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}, zones_after={zones_after}, zones_before={zones_before}, timer_active={self.timer_active}, pintu_buka_timer={self.pintu_buka_timer}")
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
elif label == "pintu-tutup" and self.timer_active 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 = {}
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
#logger.info("Start Counting, Timer Counting 30-minutes 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_counting)
|
||||||
|
#timer_thread.daemon = True
|
||||||
|
#timer_thread.start()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Call to RPI
|
||||||
|
self.action_relay_on()
|
||||||
|
|
||||||
|
def check_timer_expiration_counting(self):
|
||||||
|
"""Check if timer has expired (30 minutes)"""
|
||||||
|
time.sleep(30 * 60) # Wait 5 minutes
|
||||||
|
|
||||||
|
if self.timer_active:
|
||||||
|
logger.info("Stop Counting. Timer Counting Expired (30 minutes)")
|
||||||
|
self.timer_active = False
|
||||||
|
self.pintu_kiri_buka_detected = False
|
||||||
|
self.pintu_kanan_buka_detected = False
|
||||||
|
self.seen_objects = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Call RPi
|
||||||
|
#if not self.timer_active:
|
||||||
|
# self.action_relay_off()
|
||||||
|
|
||||||
|
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"
|
||||||
|
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()
|
||||||
@@ -0,0 +1,546 @@
|
|||||||
|
#!/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
|
||||||
|
import requests
|
||||||
|
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}")
|
||||||
|
|
||||||
|
# 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 = "/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.timer_active_pintu_tutup = False
|
||||||
|
self.timer_start_time_pintu_tutup = 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 action_relay_on(self):
|
||||||
|
try:
|
||||||
|
requests.get(self.relay_on, timeout=1)
|
||||||
|
logger.info("Relay ON")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
def action_relay_off(self):
|
||||||
|
try:
|
||||||
|
requests.get(self.relay_off, timeout=2)
|
||||||
|
logger.info("Relay OFF")
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
logger.info(e)
|
||||||
|
|
||||||
|
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 and:
|
||||||
|
if stationary and label == "karung":
|
||||||
|
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}, zones_after={zones_after}, zones_before={zones_before}, timer_active={self.timer_active}, pintu_buka_timer={self.pintu_buka_timer}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle different object types
|
||||||
|
if (
|
||||||
|
label == "pintu-kiri-buka"
|
||||||
|
and not self.timer_active
|
||||||
|
and not self.pintu_kiri_buka_detected
|
||||||
|
and not self.timer_active_pintu_tutup
|
||||||
|
):
|
||||||
|
self.handle_pintu_kiri_buka(camera_name)
|
||||||
|
elif (
|
||||||
|
label == "pintu-kanan-buka"
|
||||||
|
and not self.timer_active
|
||||||
|
and not self.pintu_kanan_buka_detected
|
||||||
|
and not self.timer_active_pintu_tutup
|
||||||
|
):
|
||||||
|
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:
|
||||||
|
elif (
|
||||||
|
label == "pintu-tutup"
|
||||||
|
and self.timer_active
|
||||||
|
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 = {}
|
||||||
|
|
||||||
|
if not self.timer_active_pintu_tutup:
|
||||||
|
self.start_timer_pintu_tutup()
|
||||||
|
|
||||||
|
# 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}")
|
||||||
|
|
||||||
|
# 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_tutup(self):
|
||||||
|
"""Start the 30-seconds timer Pintu"""
|
||||||
|
logger.info("Starting 30-seconds timer Pintu Tutup")
|
||||||
|
self.timer_active_pintu_tutup = True
|
||||||
|
self.timer_start_time_pintu_tutup = datetime.now()
|
||||||
|
|
||||||
|
# Schedule timer expiration check
|
||||||
|
timer_thread = threading.Thread(target=self.check_timer_expiration_pintu_tutup)
|
||||||
|
timer_thread.daemon = True
|
||||||
|
timer_thread.start()
|
||||||
|
|
||||||
|
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")
|
||||||
|
# logger.info("Start Counting, Timer Counting 30-minutes 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_counting)
|
||||||
|
# timer_thread.daemon = True
|
||||||
|
# timer_thread.start()
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Call to RPI
|
||||||
|
self.action_relay_on()
|
||||||
|
|
||||||
|
def check_timer_expiration_counting(self):
|
||||||
|
"""Check if timer has expired (30 minutes)"""
|
||||||
|
time.sleep(30 * 60) # Wait 5 minutes
|
||||||
|
|
||||||
|
if self.timer_active:
|
||||||
|
logger.info("Stop Counting. Timer Counting Expired (30 minutes)")
|
||||||
|
self.timer_active = False
|
||||||
|
self.pintu_kiri_buka_detected = False
|
||||||
|
self.pintu_kanan_buka_detected = False
|
||||||
|
self.seen_objects = {}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Call RPi
|
||||||
|
# if not self.timer_active:
|
||||||
|
# self.action_relay_off()
|
||||||
|
|
||||||
|
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 check_timer_expiration_pintu_tutup(self):
|
||||||
|
"""Check if timer has expired (30 seconds)"""
|
||||||
|
time.sleep(30) # Wait 5 minutes
|
||||||
|
|
||||||
|
if self.timer_active_pintu_tutup:
|
||||||
|
logger.info("Timer Pintu Tutup expired (30 seconds)")
|
||||||
|
self.timer_active_pintu_tutup = 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()
|
||||||
@@ -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.
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"kandang_1_karung_masuk": {
|
||||||
|
"karung": 3
|
||||||
|
},
|
||||||
|
"kandang_1_karung_masuk_out": {}
|
||||||
|
}
|
||||||
+2
-1
@@ -1,2 +1,3 @@
|
|||||||
paho-mqtt>=1.6.1
|
paho-mqtt>=1.6.1
|
||||||
schedule>=1.2.0
|
schedule>=1.2.0
|
||||||
|
flask>=2.0.0
|
||||||
Executable
+29
@@ -0,0 +1,29 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Start the Flask web application
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
# Check if virtual environment exists, if not create it
|
||||||
|
if [ ! -d "venv" ]; then
|
||||||
|
echo "Creating virtual environment..."
|
||||||
|
python3 -m venv venv
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Activate virtual environment
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
echo "Installing dependencies..."
|
||||||
|
pip install -q -r requirements.txt
|
||||||
|
|
||||||
|
# Check if database exists
|
||||||
|
if [ ! -f "karung_counts.db" ]; then
|
||||||
|
echo "Error: Database karung_counts.db not found!"
|
||||||
|
echo "Please run the frigate_counter.py first to initialize the database."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the Flask application
|
||||||
|
echo "Starting Flask web application..."
|
||||||
|
echo "Access the dashboard at: http://localhost:5000"
|
||||||
|
python web_app.py
|
||||||
@@ -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>
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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
@@ -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=8080, debug=True)
|
||||||
Reference in new issue
Block a user