fix no activity data not recorder, so 0 data is still recorded on db

This commit is contained in:
Alberto-Audrix committed 2026-09-09 16:19:11 +07:00
1 parent 47e9381269
commit 6639c4f7ee
2 files changed
+147 -62

No files matched your search

+80 -61
View File
@@ -43,6 +43,8 @@ CONTROL_FILE = os.getenv("CONTROL_FILE", f"{_DEFAULT_DIR}/control.json")
CONTROL_DEFAULT_COUNTING = os.getenv("CONTROL_DEFAULT_COUNTING", "true").lower() == "true"
SITE_NAME = os.getenv("SITE_NAME", "LIVE")
CAMERA_NAME = os.getenv("CAMERA_NAME", "CC1")
OBJECT_LABEL = os.getenv("OBJECT_LABEL", "object")
DASHBOARD_PORT = int(os.getenv("DASHBOARD_PORT", "5000"))
DASHBOARD_HOST = os.getenv("DASHBOARD_HOST", "0.0.0.0")
@@ -291,8 +293,61 @@ def _load_current_state():
}
def _zero_day_row(date_str, camera_name=None, object_label=None):
"""Stub row for a counting day with no activity."""
return {
"date": date_str,
"camera_name": camera_name or CAMERA_NAME,
"object_label": object_label or OBJECT_LABEL,
"total_count": 0,
"total_in": 0,
"total_out": 0,
"diff": 0,
"start_time": None,
"end_time": None,
"updated_at": None,
}
def _fill_date_gaps(rows, date_from=None, date_to=None, descending=True):
"""Ensure every date in [date_from, date_to] is present; missing days are 0."""
by_date = {r["date"]: r for r in rows}
if date_to is None:
date_to = get_counting_date()
if date_from is None:
if not by_date:
return rows
date_from = min(by_date.keys())
sample = next(iter(by_date.values()), None)
cam = sample["camera_name"] if sample else CAMERA_NAME
lab = sample["object_label"] if sample else OBJECT_LABEL
filled = []
d = datetime.strptime(date_from, "%Y-%m-%d").date()
end = datetime.strptime(date_to, "%Y-%m-%d").date()
if d > end:
return rows
while d <= end:
key = d.isoformat()
if key in by_date:
filled.append(by_date[key])
else:
filled.append(_zero_day_row(key, cam, lab))
d += timedelta(days=1)
if descending:
filled.reverse()
return filled
def _query_history(date_from=None, date_to=None, days=None, limit=None, offset=0):
"""Query daily_counters with optional date range and pagination."""
"""Query daily_counters with optional date range and pagination.
Missing dates in the requested range are filled with zero totals so the API
always returns a continuous series.
"""
conn = get_db()
cur = conn.cursor()
@@ -311,22 +366,14 @@ def _query_history(date_from=None, date_to=None, days=None, limit=None, offset=0
where = f"WHERE {' AND '.join(clauses)}" if clauses else ""
cur.execute(f"SELECT COUNT(*) AS total FROM daily_counters {where}", params)
total = cur.fetchone()["total"]
sql = f"""
SELECT counting_date, camera_name, object_label,
total_count, total_in, total_out, start_time, end_time, updated_at
FROM daily_counters
{where}
ORDER BY counting_date DESC
ORDER BY counting_date ASC
"""
page_params = list(params)
if limit is not None:
sql += " LIMIT ? OFFSET ?"
page_params.extend([limit, offset])
cur.execute(sql, page_params)
cur.execute(sql, params)
rows = [
{
"date": row["counting_date"],
@@ -343,6 +390,12 @@ def _query_history(date_from=None, date_to=None, days=None, limit=None, offset=0
for row in cur.fetchall()
]
conn.close()
fill_to = date_to or get_counting_date()
rows = _fill_date_gaps(rows, date_from=date_from, date_to=fill_to, descending=True)
total = len(rows)
if limit is not None:
rows = rows[offset: offset + limit]
return rows, total
@@ -504,18 +557,11 @@ def api_history_day(counting_date):
try:
rows, _ = _query_history(date_from=counting_date, date_to=counting_date)
if not rows:
return jsonify({
"success": False,
"error": {
"code": "NOT_FOUND",
"message": f"No history found for counting date {counting_date}",
},
}), 404
data = rows[0] if rows else _zero_day_row(counting_date)
return jsonify({
"success": True,
"site_name": SITE_NAME,
"data": rows[0] if len(rows) == 1 else rows,
"data": data,
})
except sqlite3.OperationalError as e:
return jsonify({
@@ -624,27 +670,19 @@ def api_daily_data():
try:
days = request.args.get("days", 30, type=int)
date_from = (datetime.now() - timedelta(days=days)).date().isoformat()
conn = get_db()
cur = conn.cursor()
cur.execute(
"""
SELECT counting_date, total_count, total_in, total_out
FROM daily_counters
WHERE counting_date >= ?
ORDER BY counting_date ASC
""",
(date_from,),
)
date_to = get_counting_date()
rows, _ = _query_history(date_from=date_from, date_to=date_to)
# ASC for charts
rows = list(reversed(rows))
daily_data = [
{
"date": row["counting_date"],
"date": row["date"],
"total_count": row["total_count"],
"total_in": row["total_in"],
"total_out": row["total_out"],
}
for row in cur.fetchall()
for row in rows
]
conn.close()
return jsonify(daily_data)
except sqlite3.OperationalError:
return jsonify([]), 200
@@ -655,27 +693,18 @@ def api_daily_data():
@app.route("/api/available-dates")
def api_available_dates():
try:
conn = get_db()
cur = conn.cursor()
cur.execute(
"""
SELECT counting_date, total_count, total_in, total_out, start_time, end_time
FROM daily_counters
ORDER BY counting_date DESC
"""
)
rows, _ = _query_history()
dates = [
{
"date": row["counting_date"],
"date": row["date"],
"total_count": row["total_count"],
"total_in": row["total_in"],
"total_out": row["total_out"],
"start_time": row["start_time"],
"end_time": row["end_time"],
}
for row in cur.fetchall()
for row in rows
]
conn.close()
return jsonify(dates)
except sqlite3.OperationalError:
return jsonify([]), 200
@@ -725,19 +754,9 @@ def export_daily_xlsx():
try:
days = request.args.get("days", 30, type=int)
date_from = (datetime.now() - timedelta(days=days)).date().isoformat()
conn = get_db()
cur = conn.cursor()
cur.execute(
"""
SELECT counting_date, total_count, total_in, total_out, start_time, end_time
FROM daily_counters
WHERE counting_date >= ?
ORDER BY counting_date ASC
""",
(date_from,),
)
rows = cur.fetchall()
conn.close()
date_to = get_counting_date()
rows, _ = _query_history(date_from=date_from, date_to=date_to)
rows = list(reversed(rows)) # ASC for export
except sqlite3.OperationalError as e:
return jsonify({"success": False, "error": f"Database unavailable: {e}"}), 503
except Exception as e:
@@ -749,11 +768,11 @@ def export_daily_xlsx():
_style_header(ws, [("A", "Date"), ("B", "Total"), ("C", "In"), ("D", "Out"), ("E", "Diff (In-Out)"), ("F", "First Count"), ("G", "Last Count")])
for r_idx, row in enumerate(rows, 2):
ws.cell(row=r_idx, column=1, value=row["counting_date"])
ws.cell(row=r_idx, column=1, value=row["date"])
ws.cell(row=r_idx, column=2, value=row["total_count"])
ws.cell(row=r_idx, column=3, value=row["total_in"])
ws.cell(row=r_idx, column=4, value=row["total_out"])
ws.cell(row=r_idx, column=5, value=(row["total_in"] or 0) - (row["total_out"] or 0))
ws.cell(row=r_idx, column=5, value=row["diff"])
ws.cell(row=r_idx, column=6, value=row["start_time"])
ws.cell(row=r_idx, column=7, value=row["end_time"])
+67 -1
View File
@@ -41,6 +41,12 @@ class CounterStore:
self.db = sqlite3.connect(db_path, check_same_thread=False)
self._init_db()
self.current_state = self._load_state()
with self.state_lock:
self._fill_missing_days()
counting_date = self.get_counting_date()
if self.current_state is None:
self._start_new_day(counting_date)
self._persist_day()
def _init_db(self):
cur = self.db.cursor()
@@ -137,6 +143,60 @@ class CounterStore:
self.save_state()
self.log(f'Started counting day {counting_date} ({self.object_label})')
def _ensure_day_row(self, counting_date, start_time=None, end_time=None):
"""Insert a zero row for counting_date if it does not already exist."""
now = datetime.now().isoformat()
cur = self.db.cursor()
cur.execute(
"""
INSERT OR IGNORE INTO daily_counters
(counting_date, camera_name, object_label,
total_count, total_in, total_out, start_time, end_time)
VALUES (?, ?, ?, 0, 0, 0, ?, ?)
""",
(
counting_date,
self.camera_name,
self.object_label,
start_time or now,
end_time or now,
),
)
self.db.commit()
def _fill_missing_days(self):
"""Backfill any missing counting dates from first DB row through today as 0."""
today = self.get_counting_date()
cur = self.db.cursor()
cur.execute(
"""
SELECT counting_date FROM daily_counters
WHERE camera_name = ? AND object_label = ?
ORDER BY counting_date ASC
""",
(self.camera_name, self.object_label),
)
existing = {row[0] for row in cur.fetchall()}
if not existing:
self._ensure_day_row(today)
return
start = datetime.strptime(min(existing), '%Y-%m-%d').date()
end = datetime.strptime(today, '%Y-%m-%d').date()
filled = 0
d = start
while d <= end:
key = d.isoformat()
if key not in existing:
self._ensure_day_row(key)
filled += 1
d += timedelta(days=1)
if filled:
self.log(
f'Backfilled {filled} zero-activity day(s) '
f'{start.isoformat()}..{end.isoformat()}'
)
def record_object_crossing(self, track_id, direction):
"""Record an object crossing a counting line. direction: 'in' | 'out'.
@@ -199,13 +259,19 @@ class CounterStore:
while not self.shutdown_event.is_set():
time.sleep(60)
with self.state_lock:
self._fill_missing_days()
counting_date = self.get_counting_date()
if self.current_state is None:
self._start_new_day(counting_date)
self._persist_day()
continue
if self.current_state['counting_date'] != self.get_counting_date():
if self.current_state['counting_date'] != counting_date:
self.log('Daily cutoff reached - finalizing day totals')
self._persist_day()
self.current_state = None
self.save_state()
self._start_new_day(counting_date)
self._persist_day()
def start_cutoff_watcher(self):
t = threading.Thread(target=self.cutoff_watcher_loop, daemon=True)