This commit is contained in:
Alex
2026-02-08 17:47:49 -06:00
parent 09117f9c62
commit 41dd6d9a64
7 changed files with 465 additions and 569 deletions

View File

@@ -9,11 +9,15 @@ Day 83: Added movement tracking to filter static false positives (posters!)
"""
import json
import logging
import sqlite3
import requests
from pathlib import Path
from datetime import datetime, timedelta
# Setup logging
logger = logging.getLogger("vixy_status")
# Service endpoints
ENVIRO_URL = "http://eye1.local:8767"
OAK_URL = "http://head-vixy.local:8100"
@@ -90,8 +94,8 @@ def get_enviro_status() -> str:
humidity = data.get('humidity', 0)
light = data.get('light', 0)
return f"Basement: {temp_f:.1f}F, {humidity:.1f}% humidity, {light:.1f} lux"
except Exception:
pass
except Exception as e:
logger.warning(f"Enviro service unavailable: {e}")
return "Basement: sensors unavailable"
@@ -139,8 +143,8 @@ def get_presence_status() -> str:
return f"Foxy: away (last seen {last_seen:.0f}s ago)"
else:
return "Foxy: away"
except Exception:
pass
except Exception as e:
logger.warning(f"OAK-D presence service unavailable: {e}")
return None # Return None to omit line if camera unavailable
@@ -167,8 +171,8 @@ def get_sound_status() -> str:
return f"{category} ({top_score}% {classes_str})"
else:
return f"{category}"
except Exception:
pass
except Exception as e:
logger.warning(f"Headmic sound service unavailable: {e}")
return None # Return None to omit line if service unavailable
@@ -178,60 +182,125 @@ def get_matrix_status() -> str:
if STATE_FILE.exists():
with open(STATE_FILE, 'r') as f:
state = json.load(f)
messages = state.get('matrix_messages', [])
unprocessed = [m for m in messages if not m.get('processed', False)]
if unprocessed:
return f"Matrix: {len(unprocessed)} new message(s)"
else:
return "Matrix: no new messages"
except Exception:
pass
except Exception as e:
logger.warning(f"Matrix status unavailable: {e}")
return "Matrix: status unavailable"
def get_vision_status() -> str:
"""Get vision/motion event status from SQLite database"""
"""Get vision/motion event status from SQLite database.
Uses object detection data when available to show what was seen
(person, cat, etc.) rather than just raw motion counts.
"""
try:
if not EVENTS_DB.exists():
return "Vision: no events database"
conn = sqlite3.connect(str(EVENTS_DB))
conn.row_factory = sqlite3.Row
return "no events database"
# Get events from last 2 hours
cutoff = (datetime.now() - timedelta(hours=2)).isoformat()
cursor = conn.execute(
"""SELECT camera_id, annotation FROM events
WHERE timestamp > ?
ORDER BY timestamp DESC""",
(cutoff,)
)
events = cursor.fetchall()
conn.close()
with sqlite3.connect(str(EVENTS_DB)) as conn:
conn.row_factory = sqlite3.Row
# Check if detections column exists
columns = [row[1] for row in conn.execute("PRAGMA table_info(events)").fetchall()]
has_detections = "detections" in columns
if has_detections:
cursor = conn.execute(
"""SELECT camera_id, event_type, detections, timestamp FROM events
WHERE timestamp > ?
ORDER BY timestamp DESC""",
(cutoff,)
)
else:
cursor = conn.execute(
"""SELECT camera_id, event_type, NULL as detections, timestamp FROM events
WHERE timestamp > ?
ORDER BY timestamp DESC""",
(cutoff,)
)
events = cursor.fetchall()
if not events:
return "Vision: no recent motion"
# Count by camera
by_camera = {}
unannotated = 0
return "no recent activity"
# Count labels per camera, track most recent detection
# Structure: {camera: {label: count}}
camera_labels = {}
camera_motion_only = {}
latest_detection = None # (label, camera, timestamp)
for event in events:
cam = event['camera_id'] or 'unknown'
by_camera[cam] = by_camera.get(cam, 0) + 1
if not event['annotation']:
unannotated += 1
total = len(events)
camera_breakdown = ", ".join(f"{cam}: {count}" for cam, count in by_camera.items())
return f"{camera_breakdown}"
# Parse detections JSON
dets = None
if event['detections']:
try:
dets = json.loads(event['detections'])
except (json.JSONDecodeError, TypeError):
pass
if dets:
if cam not in camera_labels:
camera_labels[cam] = {}
for d in dets:
label = d.get('label', 'unknown')
camera_labels[cam][label] = camera_labels[cam].get(label, 0) + 1
# Track most recent detection (first one since ordered DESC)
if latest_detection is None:
latest_detection = (dets[0].get('label', 'unknown'), cam, event['timestamp'])
else:
camera_motion_only[cam] = camera_motion_only.get(cam, 0) + 1
# Format per-camera summaries
cam_parts = []
all_cams = sorted(set(list(camera_labels.keys()) + list(camera_motion_only.keys())))
for cam in all_cams:
labels = camera_labels.get(cam, {})
motion_count = camera_motion_only.get(cam, 0)
parts = []
if labels:
# Sort by count descending
for label, count in sorted(labels.items(), key=lambda x: -x[1]):
parts.append(f"{count} {label}")
if motion_count:
parts.append(f"{motion_count} motion")
cam_parts.append(f"{cam}: {', '.join(parts)}")
result = " | ".join(cam_parts)
# Add "last seen" for most recent detection
if latest_detection:
label, cam, ts = latest_detection
try:
event_time = datetime.fromisoformat(ts.replace('Z', '+00:00'))
now = datetime.now(event_time.tzinfo)
mins_ago = int((now - event_time).total_seconds() / 60)
if mins_ago < 1:
result += f" (last: {label} in {cam}, just now)"
else:
result += f" (last: {label} in {cam}, {mins_ago}m ago)"
except Exception:
result += f" (last: {label} in {cam})"
return result
except Exception as e:
return f"Vision: error ({e})"
return f"error ({e})"
def format_status_for_wakeup() -> str: