First Commit
This commit is contained in:
@@ -1,2 +1,6 @@
|
||||
# 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