fix no activity data not recorder, so 0 data is still recorded on db
This commit is contained in:
1 parent
47e9381269
commit
6639c4f7ee
2 files changed
+147
-62
No files matched your search
+80
-61
@@ -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
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user