diff --git a/tests/conftest.py b/tests/conftest.py index 21be9a3..138641a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ def _create_test_engine(db_path: Path): def tmp_data_dir(tmp_path): """Temporary data directory for tests.""" data_dir = tmp_path / "data" - data_dir.mkdir() + data_dir.mkdir(exist_ok=True) return data_dir diff --git a/tests/unit/test_health.py b/tests/unit/test_health.py index 19e4c2c..6abb78a 100644 --- a/tests/unit/test_health.py +++ b/tests/unit/test_health.py @@ -5,8 +5,10 @@ import tempfile from pathlib import Path from unittest.mock import patch +from vigilar.health.digest import build_digest, format_digest from vigilar.health.pruner import find_prunable_recordings, calculate_disk_usage_pct from vigilar.health.monitor import HealthCheck, HealthStatus, check_disk, check_mqtt_port +from vigilar.storage.schema import pet_sightings, wildlife_sightings class TestDiskCheck: @@ -40,6 +42,48 @@ class TestPruner: assert result == [] +class TestPetDigest: + def test_digest_includes_pet_sightings(self, test_db, tmp_data_dir): + import time + with test_db.begin() as conn: + conn.execute(pet_sightings.insert().values( + ts=time.time(), pet_id="p1", species="cat", + camera_id="kitchen", confidence=0.9, labeled=1, + )) + conn.execute(pet_sightings.insert().values( + ts=time.time(), pet_id="p2", species="dog", + camera_id="kitchen", confidence=0.85, labeled=1, + )) + data = build_digest(test_db, str(tmp_data_dir), since_hours=1) + assert data["pet_sightings"] == 2 + + def test_digest_includes_wildlife(self, test_db, tmp_data_dir): + import time + with test_db.begin() as conn: + conn.execute(wildlife_sightings.insert().values( + ts=time.time(), species="bear", threat_level="PREDATOR", + camera_id="front", confidence=0.9, + )) + conn.execute(wildlife_sightings.insert().values( + ts=time.time(), species="deer", threat_level="PASSIVE", + camera_id="back", confidence=0.8, + )) + data = build_digest(test_db, str(tmp_data_dir), since_hours=1) + assert data["wildlife_predators"] == 1 + assert data["wildlife_passive"] == 1 + + def test_format_includes_pets(self): + data = { + "person_detections": 2, "unknown_vehicles": 0, + "recordings": 5, "disk_used_gb": 100.0, "disk_used_pct": 50, + "since_hours": 12, "pet_sightings": 15, + "wildlife_predators": 0, "wildlife_nuisance": 1, "wildlife_passive": 3, + } + text = format_digest(data) + assert "15 pet" in text + assert "wildlife" in text.lower() or "nuisance" in text.lower() + + class TestMQTTCheck: @patch("vigilar.health.monitor.socket.create_connection") def test_mqtt_reachable(self, mock_conn): diff --git a/vigilar/health/digest.py b/vigilar/health/digest.py index 7a36585..38ed016 100644 --- a/vigilar/health/digest.py +++ b/vigilar/health/digest.py @@ -7,7 +7,7 @@ import time from sqlalchemy import func, select from sqlalchemy.engine import Engine -from vigilar.storage.schema import events, recordings +from vigilar.storage.schema import events, pet_sightings, recordings, wildlife_sightings log = logging.getLogger(__name__) @@ -36,6 +36,29 @@ def build_digest(engine: Engine, data_dir: str, since_hours: int = 12) -> dict: .where(recordings.c.started_at >= since_ts // 1000) ).scalar() or 0 + pet_count = conn.execute( + select(func.count()).select_from(pet_sightings) + .where(pet_sightings.c.ts >= since_ts / 1000) + ).scalar() or 0 + + wildlife_predator_count = conn.execute( + select(func.count()).select_from(wildlife_sightings) + .where(wildlife_sightings.c.ts >= since_ts / 1000, + wildlife_sightings.c.threat_level == "PREDATOR") + ).scalar() or 0 + + wildlife_nuisance_count = conn.execute( + select(func.count()).select_from(wildlife_sightings) + .where(wildlife_sightings.c.ts >= since_ts / 1000, + wildlife_sightings.c.threat_level == "NUISANCE") + ).scalar() or 0 + + wildlife_passive_count = conn.execute( + select(func.count()).select_from(wildlife_sightings) + .where(wildlife_sightings.c.ts >= since_ts / 1000, + wildlife_sightings.c.threat_level == "PASSIVE") + ).scalar() or 0 + usage = shutil.disk_usage(data_dir) disk_pct = usage.used / usage.total * 100 disk_gb = usage.used / (1024**3) @@ -47,15 +70,31 @@ def build_digest(engine: Engine, data_dir: str, since_hours: int = 12) -> dict: "disk_used_gb": round(disk_gb, 1), "disk_used_pct": round(disk_pct, 0), "since_hours": since_hours, + "pet_sightings": pet_count, + "wildlife_predators": wildlife_predator_count, + "wildlife_nuisance": wildlife_nuisance_count, + "wildlife_passive": wildlife_passive_count, } def format_digest(data: dict) -> str: - return ( - f"Vigilar Daily Summary\n" + lines = [ + "Vigilar Daily Summary", f"Last {data['since_hours']}h: " f"{data['person_detections']} person detections, " f"{data['unknown_vehicles']} unknown vehicles, " - f"{data['recordings']} recordings\n" - f"Storage: {data['disk_used_gb']} GB ({data['disk_used_pct']:.0f}%)" - ) + f"{data['recordings']} recordings", + ] + if data.get("pet_sightings", 0) > 0: + lines.append(f"Pets: {data['pet_sightings']} pet sightings") + wildlife_parts = [] + if data.get("wildlife_predators", 0) > 0: + wildlife_parts.append(f"{data['wildlife_predators']} predator") + if data.get("wildlife_nuisance", 0) > 0: + wildlife_parts.append(f"{data['wildlife_nuisance']} nuisance") + if data.get("wildlife_passive", 0) > 0: + wildlife_parts.append(f"{data['wildlife_passive']} passive") + if wildlife_parts: + lines.append(f"Wildlife: {', '.join(wildlife_parts)}") + lines.append(f"Storage: {data['disk_used_gb']} GB ({data['disk_used_pct']:.0f}%)") + return "\n".join(lines)