First Commit
This commit is contained in:
@@ -1,2 +1,6 @@
|
|||||||
# frigate_counter
|
# frigate_counter
|
||||||
|
to be installed in /etc/frigate
|
||||||
|
|
||||||
|
apt install python3-flask
|
||||||
|
|
||||||
|
frigate_counter.py -> as frigate_counter service
|
||||||
|
|||||||
337
frigate_counter.py
Normal file
337
frigate_counter.py
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import json
|
||||||
|
import threading
|
||||||
|
import paho.mqtt.client as mqtt
|
||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# SITE CONFIG
|
||||||
|
# =====================
|
||||||
|
TOP_TOPIC = "cpsp"
|
||||||
|
SITE_NAME = "sukawarna"
|
||||||
|
SITE_TOPIC = f"{TOP_TOPIC}/counter/{SITE_NAME}"
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# KONFIGURASI MQTT
|
||||||
|
# =====================
|
||||||
|
BROKER = "localhost" # ganti sesuai broker
|
||||||
|
PORT = 1883
|
||||||
|
USERNAME = "" # ganti user
|
||||||
|
PASSWORD = "" # ganti password
|
||||||
|
TOPIC = "frigate/events"
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# MQTT PUBLISH
|
||||||
|
# =====================
|
||||||
|
PUBLISH_BROKER = "mqtt.backone.cloud"
|
||||||
|
PUBLISH_PORT = 1883
|
||||||
|
USERNAME = "" # ganti user
|
||||||
|
PASSWORD = "" # ganti password
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# FILE COUNTER
|
||||||
|
# =====================
|
||||||
|
DATA_FILE = "/etc/frigate/counter_data.json"
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# DATABASE INITIALIZATION
|
||||||
|
# =====================
|
||||||
|
DB_FILE = "/etc/frigate/counter_database.db"
|
||||||
|
|
||||||
|
|
||||||
|
def init_database():
|
||||||
|
"""Initialize the SQLite database with the required table"""
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Create table if it doesn't exist
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS counter_data (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
camera_name TEXT NOT NULL,
|
||||||
|
date TIMESTAMP NOT NULL,
|
||||||
|
counter_value INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# Initialize the database
|
||||||
|
init_database()
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# INISIALISASI COUNTER
|
||||||
|
# =====================
|
||||||
|
if os.path.exists(DATA_FILE):
|
||||||
|
with open(DATA_FILE, "r") as f:
|
||||||
|
counter = json.load(f)
|
||||||
|
else:
|
||||||
|
counter = {
|
||||||
|
"kandang_1_karung_pakan": {"karung": 0},
|
||||||
|
"kandang_2_karung_pakan": {"karung": 0},
|
||||||
|
"kandang_1_karung_masuk": {"karung": 0},
|
||||||
|
"kandang_atas_feeder_kiri": {"karung": 0},
|
||||||
|
"kandang_bawah_feeder_kanan": {"karung": 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
camera_feeder_no_duplicate = [
|
||||||
|
"kandang_1_karung_pakan",
|
||||||
|
"kandang_2_karung_pakan"
|
||||||
|
]
|
||||||
|
|
||||||
|
camera_feeder = [
|
||||||
|
"kandang_1_karung_pakan",
|
||||||
|
"kandang_2_karung_pakan",
|
||||||
|
"kandang_atas_feeder_kiri",
|
||||||
|
"kandang_bawah_feeder_kanan"
|
||||||
|
]
|
||||||
|
|
||||||
|
camera_masuk = ["kandang_1_karung_masuk"]
|
||||||
|
|
||||||
|
# Untuk debounce object per track_id
|
||||||
|
seen_objects = {} # dict per kamera: {track_id: last_seen}
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# FUNGSI SAVE COUNTER
|
||||||
|
# =====================
|
||||||
|
def save_counter():
|
||||||
|
with open(DATA_FILE, "w") as f:
|
||||||
|
json.dump(counter, f, indent=2)
|
||||||
|
|
||||||
|
|
||||||
|
def save_counter_to_database(camera_list_to_save=[]):
|
||||||
|
"""Save current counter values to SQLite database"""
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get current timestamp
|
||||||
|
current_time = datetime.now()
|
||||||
|
|
||||||
|
# Save each counter value to database
|
||||||
|
for camera_name, camera_data in counter.items():
|
||||||
|
for label, value in camera_data.items():
|
||||||
|
if label == "karung" and camera_name in camera_list_to_save: # Only save karung counters
|
||||||
|
cursor.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO counter_data (camera_name, date, counter_value)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""",
|
||||||
|
(camera_name, current_time, value),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def republish_counter():
|
||||||
|
# Publish current counter
|
||||||
|
for camera, v in counter.items():
|
||||||
|
for label, value in v.items():
|
||||||
|
if label == "karung":
|
||||||
|
print(f"{SITE_TOPIC}/{camera}/{label}", value)
|
||||||
|
client_publish.publish(
|
||||||
|
f"{SITE_TOPIC}/{camera}/{label}", value, qos=1, retain=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# RESET HARIAN
|
||||||
|
# =====================
|
||||||
|
def reset_counter():
|
||||||
|
global counter, seen_objects
|
||||||
|
print(f"[{datetime.now()}] Reset counter otomatis")
|
||||||
|
# Save current counter values to database before reset
|
||||||
|
save_counter_to_database(camera_feeder)
|
||||||
|
# reset semua
|
||||||
|
# for cam in counter:
|
||||||
|
# for obj in counter[cam]:
|
||||||
|
# counter[cam][obj] = 0
|
||||||
|
for camera_name in camera_feeder:
|
||||||
|
counter[camera_name]["karung"] = 0
|
||||||
|
|
||||||
|
"""
|
||||||
|
counter["kandang_1_karung_pakan"]["karung"] = 0
|
||||||
|
counter["kandang_2_karung_pakan"]["karung"] = 0
|
||||||
|
counter["kandang_atas_feeder_kiri"]["karung"] = 0
|
||||||
|
counter["kandang_bawah_feeder_kanan"]["karung"] = 0
|
||||||
|
"""
|
||||||
|
seen_objects = {}
|
||||||
|
save_counter()
|
||||||
|
schedule_reset() # jadwalkan besok
|
||||||
|
republish_counter()
|
||||||
|
|
||||||
|
|
||||||
|
def reset_counter_masuk():
|
||||||
|
global counter, seen_objects
|
||||||
|
print(f"[{datetime.now()}] Reset counter otomatis")
|
||||||
|
# Save current counter values to database before reset
|
||||||
|
save_counter_to_database(camera_masuk)
|
||||||
|
# reset semua
|
||||||
|
# for cam in counter:
|
||||||
|
# for obj in counter[cam]:
|
||||||
|
# counter[cam][obj] = 0
|
||||||
|
for camera_name in camera_masuk:
|
||||||
|
counter[camera_name]["karung"] = 0
|
||||||
|
|
||||||
|
#counter["kandang_1_karung_masuk"]["karung"] = 0
|
||||||
|
|
||||||
|
seen_objects = {}
|
||||||
|
save_counter()
|
||||||
|
schedule_reset_masuk() # jadwalkan besok
|
||||||
|
republish_counter()
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_reset():
|
||||||
|
now = datetime.now()
|
||||||
|
reset_time = now.replace(hour=17, minute=0, second=0, microsecond=0)
|
||||||
|
if now >= reset_time:
|
||||||
|
reset_time += timedelta(days=1)
|
||||||
|
delay = (reset_time - now).total_seconds()
|
||||||
|
threading.Timer(delay, reset_counter).start()
|
||||||
|
print(f"Reset counter dijadwalkan pada: {reset_time}")
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_reset_masuk():
|
||||||
|
now = datetime.now()
|
||||||
|
reset_time = now.replace(hour=23, minute=59, second=55, microsecond=0)
|
||||||
|
if now >= reset_time:
|
||||||
|
reset_time += timedelta(days=1)
|
||||||
|
delay = (reset_time - now).total_seconds()
|
||||||
|
threading.Timer(delay, reset_counter_masuk).start()
|
||||||
|
print(f"Reset counter dijadwalkan pada: {reset_time}")
|
||||||
|
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# CALLBACK MQTT
|
||||||
|
# =====================
|
||||||
|
def on_connect(client, userdata, flags, rc):
|
||||||
|
print("Connected to MQTT with result code " + str(rc))
|
||||||
|
client.subscribe(TOPIC)
|
||||||
|
|
||||||
|
|
||||||
|
def on_connect_publish(client, userdata, flags, rc):
|
||||||
|
print("Connected to MQTT PUBLISH with result code " + str(rc))
|
||||||
|
# Publish current counter
|
||||||
|
republish_counter()
|
||||||
|
|
||||||
|
|
||||||
|
def on_message(client, userdata, msg):
|
||||||
|
try:
|
||||||
|
payload = json.loads(msg.payload.decode())
|
||||||
|
if "after" not in payload:
|
||||||
|
return
|
||||||
|
|
||||||
|
event_after = payload["after"]
|
||||||
|
event_before = payload.get("before", {})
|
||||||
|
|
||||||
|
camera = event_after.get("camera")
|
||||||
|
label = event_after.get("label")
|
||||||
|
track_id = event_after.get("id")
|
||||||
|
zones_after = event_after.get("entered_zones", [])
|
||||||
|
zones_before = event_before.get("entered_zones", [])
|
||||||
|
|
||||||
|
# Dont detect stationary
|
||||||
|
stationary = event_after.get("stationary")
|
||||||
|
if stationary:
|
||||||
|
return
|
||||||
|
|
||||||
|
# ambil zona baru yg sebelumnya belum ada
|
||||||
|
new_zones = [z for z in zones_after if z not in zones_before]
|
||||||
|
|
||||||
|
if not camera or not label:
|
||||||
|
return
|
||||||
|
if camera not in counter or label not in counter[camera]:
|
||||||
|
return
|
||||||
|
if not new_zones: # hanya hitung kalau masuk zona baru
|
||||||
|
return
|
||||||
|
|
||||||
|
# debounce per track_id
|
||||||
|
if camera not in seen_objects:
|
||||||
|
seen_objects[camera] = {}
|
||||||
|
if track_id in seen_objects[camera]:
|
||||||
|
return # sudah dihitung
|
||||||
|
|
||||||
|
# Check if timestamp is more than 30 seconds
|
||||||
|
# if camera == "kandang_1_karung_pakan" or camera == "kandang_2_karung_pakan":
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
camera == "kandang_1_karung_pakan"
|
||||||
|
or camera == "kandang_2_karung_pakan"
|
||||||
|
or camera == "kandang_atas_feeder_kiri"
|
||||||
|
or camera == "kandang_bawah_feeder_kanan"
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
if camera in camera_feeder_no_duplicate:
|
||||||
|
DETIK_ANTAR_KARUNG = 30
|
||||||
|
current_time = datetime.now()
|
||||||
|
for ts in seen_objects[camera].values():
|
||||||
|
delta = current_time - ts
|
||||||
|
if delta.seconds < DETIK_ANTAR_KARUNG:
|
||||||
|
return
|
||||||
|
|
||||||
|
# hitung
|
||||||
|
counter[camera][label] += 1
|
||||||
|
seen_objects[camera][track_id] = datetime.now()
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[{datetime.now()}] Kamera {camera}: Object {label} masuk zona {new_zones}"
|
||||||
|
)
|
||||||
|
print(f"Total {label} di {camera}: {counter[camera][label]}")
|
||||||
|
|
||||||
|
print(f"{TOP_TOPIC}/counter/{SITE_NAME}/{camera}/{label}")
|
||||||
|
|
||||||
|
# simpan dan publish
|
||||||
|
save_counter()
|
||||||
|
# client_publish.publish(f"{TOP_TOPIC}/counter/{SITE_NAME}/{camera}/{label}", counter[camera][label], qos=1, retain=True)
|
||||||
|
client_publish.publish(
|
||||||
|
f"{SITE_TOPIC}/{camera}/{label}", counter[camera][label], qos=1, retain=True
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("Error parsing message:", e)
|
||||||
|
|
||||||
|
|
||||||
|
def view_database():
|
||||||
|
"""View all records in the database"""
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM counter_data ORDER BY date DESC LIMIT 10")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
print("Last 10 records in database:")
|
||||||
|
for row in rows:
|
||||||
|
print(f"ID: {row[0]}, Camera: {row[1]}, Date: {row[2]}, Counter: {row[3]}")
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# =====================
|
||||||
|
# MAIN
|
||||||
|
# =====================
|
||||||
|
client = mqtt.Client()
|
||||||
|
client.username_pw_set(USERNAME, PASSWORD)
|
||||||
|
client.on_connect = on_connect
|
||||||
|
client.on_message = on_message
|
||||||
|
|
||||||
|
client.connect(BROKER, PORT, 60)
|
||||||
|
|
||||||
|
client_publish = mqtt.Client()
|
||||||
|
client_publish.on_connect = on_connect_publish
|
||||||
|
client_publish.connect(PUBLISH_BROKER, PUBLISH_PORT, 60)
|
||||||
|
|
||||||
|
|
||||||
|
# jadwalkan reset pertama
|
||||||
|
schedule_reset()
|
||||||
|
schedule_reset_masuk()
|
||||||
|
|
||||||
|
client_publish.loop_start()
|
||||||
|
print(f"MQTT Publish Counter berjalan di {PUBLISH_BROKER}")
|
||||||
|
print(f"MQTT Counter berjalan di {BROKER}")
|
||||||
|
client.loop_forever()
|
||||||
15
frigate_counter.service
Normal file
15
frigate_counter.service
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Frigate MQTT Counter
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/bin/python3 -u /etc/frigate/frigate_counter.py
|
||||||
|
WorkingDirectory=/etc/frigate
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
|
||||||
175
frigate_counter_web.py
Normal file
175
frigate_counter_web.py
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sqlite3
|
||||||
|
from flask import Flask, render_template, jsonify
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
DB_FILE = "/etc/frigate/counter_database.db"
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
def get_db_connection():
|
||||||
|
"""Create a database connection"""
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
conn.row_factory = sqlite3.Row # This allows us to access columns by name
|
||||||
|
return conn
|
||||||
|
|
||||||
|
def get_counter_data():
|
||||||
|
"""Get all counter data from the database"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get all records ordered by date descending
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT id, camera_name, date, counter_value
|
||||||
|
FROM counter_data
|
||||||
|
ORDER BY date DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Convert to list of dictionaries
|
||||||
|
data = []
|
||||||
|
for row in rows:
|
||||||
|
data.append({
|
||||||
|
'id': row['id'],
|
||||||
|
'camera_name': row['camera_name'],
|
||||||
|
'date': row['date'],
|
||||||
|
'counter_value': row['counter_value']
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_counter_summary():
|
||||||
|
"""Get summary of counter data grouped by camera"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get latest counter value for each camera
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT camera_name, MAX(date) as latest_date, counter_value
|
||||||
|
FROM counter_data
|
||||||
|
GROUP BY camera_name
|
||||||
|
ORDER BY latest_date DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Convert to list of dictionaries
|
||||||
|
data = []
|
||||||
|
for row in rows:
|
||||||
|
data.append({
|
||||||
|
'camera_name': row['camera_name'],
|
||||||
|
'latest_date': row['latest_date'],
|
||||||
|
'counter_value': row['counter_value']
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_pivoted_data():
|
||||||
|
"""Get counter data pivoted by camera_name (rows) and date (columns)"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Get all records ordered by date DESC (latest first)
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT camera_name, date, counter_value
|
||||||
|
FROM counter_data
|
||||||
|
ORDER BY date DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Group data by camera and date
|
||||||
|
pivoted_data = defaultdict(lambda: defaultdict(int))
|
||||||
|
dates = set()
|
||||||
|
cameras = set()
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
camera_name = row['camera_name']
|
||||||
|
date = row['date'][:10] # Extract just the date part (YYYY-MM-DD)
|
||||||
|
counter_value = row['counter_value']
|
||||||
|
|
||||||
|
pivoted_data[camera_name][date] = counter_value
|
||||||
|
dates.add(date)
|
||||||
|
cameras.add(camera_name)
|
||||||
|
|
||||||
|
# Convert sets to sorted lists (sort dates in reverse order - latest first)
|
||||||
|
sorted_dates = sorted(dates, reverse=True)
|
||||||
|
sorted_cameras = sorted(cameras)
|
||||||
|
|
||||||
|
# Create matrix format
|
||||||
|
matrix = []
|
||||||
|
for camera in sorted_cameras:
|
||||||
|
row = {'camera_name': camera}
|
||||||
|
for date in sorted_dates:
|
||||||
|
row[date] = pivoted_data[camera][date]
|
||||||
|
matrix.append(row)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'matrix': matrix,
|
||||||
|
'dates': sorted_dates,
|
||||||
|
'cameras': sorted_cameras
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
"""Main page showing counter data"""
|
||||||
|
counter_data = get_counter_data()
|
||||||
|
summary_data = get_counter_summary()
|
||||||
|
pivoted_data = get_pivoted_data()
|
||||||
|
|
||||||
|
# Add current datetime to context
|
||||||
|
current_time = datetime.now()
|
||||||
|
|
||||||
|
return render_template('index.html',
|
||||||
|
counter_data=counter_data,
|
||||||
|
summary_data=summary_data,
|
||||||
|
pivoted_data=pivoted_data,
|
||||||
|
current_time=current_time)
|
||||||
|
|
||||||
|
@app.route('/api/data')
|
||||||
|
def api_data():
|
||||||
|
"""API endpoint to get counter data as JSON"""
|
||||||
|
counter_data = get_counter_data()
|
||||||
|
return jsonify(counter_data)
|
||||||
|
|
||||||
|
@app.route('/api/summary')
|
||||||
|
def api_summary():
|
||||||
|
"""API endpoint to get counter summary as JSON"""
|
||||||
|
summary_data = get_counter_summary()
|
||||||
|
return jsonify(summary_data)
|
||||||
|
|
||||||
|
@app.route('/api/pivoted')
|
||||||
|
def api_pivoted():
|
||||||
|
"""API endpoint to get pivoted counter data as JSON"""
|
||||||
|
pivoted_data = get_pivoted_data()
|
||||||
|
return jsonify(pivoted_data)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
# Create database directory if it doesn't exist
|
||||||
|
os.makedirs(os.path.dirname(DB_FILE), exist_ok=True)
|
||||||
|
|
||||||
|
# Initialize the database if needed
|
||||||
|
conn = sqlite3.connect(DB_FILE)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS counter_data (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
camera_name TEXT NOT NULL,
|
||||||
|
date TIMESTAMP NOT NULL,
|
||||||
|
counter_value INTEGER NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
app.run(host='0.0.0.0', port=8899, debug=True)
|
||||||
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
flask
|
||||||
170
templates/index.html
Normal file
170
templates/index.html
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Frigate Counter Data</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
background-color: #e8f4f8;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.refresh-btn {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.refresh-btn:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
.last-updated {
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.matrix-table {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.matrix-table th {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
.matrix-table td {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.matrix-table tr:nth-child(even) {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
.matrix-table tr:nth-child(odd) {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Frigate Counter Data</h1>
|
||||||
|
|
||||||
|
<div class="summary">
|
||||||
|
<h2>Counter Summary</h2>
|
||||||
|
<table id="summary-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Camera Name</th>
|
||||||
|
<th>Latest Date</th>
|
||||||
|
<th>Counter Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in summary_data %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.camera_name }}</td>
|
||||||
|
<td>{{ item.latest_date }}</td>
|
||||||
|
<td>{{ item.counter_value }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Counter Data by Camera and Date</h2>
|
||||||
|
<table class="matrix-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Camera Name</th>
|
||||||
|
{% for date in pivoted_data.dates %}
|
||||||
|
<th>{{ date }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in pivoted_data.matrix %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ row.camera_name }}</td>
|
||||||
|
{% for date in pivoted_data.dates %}
|
||||||
|
<td>{{ row[date] }}</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>Full Counter Data</h2>
|
||||||
|
<table id="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Camera Name</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Counter Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in counter_data %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ item.id }}</td>
|
||||||
|
<td>{{ item.camera_name }}</td>
|
||||||
|
<td>{{ item.date }}</td>
|
||||||
|
<td>{{ item.counter_value }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="last-updated">
|
||||||
|
Last updated: {{ current_time.strftime('%Y-%m-%d %H:%M:%S') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Auto-refresh every 30 seconds
|
||||||
|
setInterval(function() {
|
||||||
|
location.reload();
|
||||||
|
}, 30000);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user