From 6e3ef1dcdcb0ac93a3e48470e8352c6e3495fda6 Mon Sep 17 00:00:00 2001 From: "Aaron D. Lee" Date: Fri, 3 Apr 2026 13:08:40 -0400 Subject: [PATCH] Add pet detection, wildlife, and activity config models Co-Authored-By: Claude Sonnet 4.6 --- tests/unit/test_config.py | 59 +++++++++++++++++++++++++++++++++++++++ vigilar/config.py | 44 +++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index aadcac0..ae2c928 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -1,6 +1,7 @@ """Tests for config loading and validation.""" from vigilar.config import CameraConfig, VigilarConfig +from vigilar.config import PetsConfig, WildlifeThreatMap, WildlifeSizeHeuristics, PetActivityConfig def test_default_config(): @@ -33,3 +34,61 @@ def test_camera_sensitivity_bounds(): import pytest with pytest.raises(Exception): CameraConfig(id="test", display_name="Test", rtsp_url="rtsp://localhost", motion_sensitivity=1.5) + + +class TestPetsConfig: + def test_defaults(self): + cfg = PetsConfig() + assert cfg.enabled is False + assert cfg.model == "yolov8s" + assert cfg.confidence_threshold == 0.5 + assert cfg.pet_id_threshold == 0.7 + assert cfg.pet_id_low_confidence == 0.5 + assert cfg.min_training_images == 20 + assert cfg.crop_retention_days == 7 + + def test_custom_values(self): + cfg = PetsConfig(enabled=True, model="yolov8m", confidence_threshold=0.6) + assert cfg.enabled is True + assert cfg.model == "yolov8m" + assert cfg.confidence_threshold == 0.6 + + +class TestWildlifeThreatMap: + def test_defaults(self): + tm = WildlifeThreatMap() + assert "bear" in tm.predator + assert "bird" in tm.passive + + def test_custom_mapping(self): + tm = WildlifeThreatMap(predator=["bear", "wolf"], nuisance=["raccoon"]) + assert "wolf" in tm.predator + assert "raccoon" in tm.nuisance + + +class TestWildlifeSizeHeuristics: + def test_defaults(self): + sh = WildlifeSizeHeuristics() + assert sh.small == 0.02 + assert sh.medium == 0.08 + assert sh.large == 0.15 + + +class TestPetActivityConfig: + def test_defaults(self): + cfg = PetActivityConfig() + assert cfg.daily_digest is True + assert cfg.highlight_clips is True + assert cfg.zoomie_threshold == 0.8 + + +class TestCameraConfigLocation: + def test_default_location_is_interior(self): + from vigilar.config import CameraConfig + cfg = CameraConfig(id="test", display_name="Test", rtsp_url="rtsp://x") + assert cfg.location == "INTERIOR" + + def test_exterior_location(self): + from vigilar.config import CameraConfig + cfg = CameraConfig(id="test", display_name="Test", rtsp_url="rtsp://x", location="EXTERIOR") + assert cfg.location == "EXTERIOR" diff --git a/vigilar/config.py b/vigilar/config.py index 75652b9..44eef74 100644 --- a/vigilar/config.py +++ b/vigilar/config.py @@ -43,6 +43,7 @@ class CameraConfig(BaseModel): resolution_capture: list[int] = Field(default_factory=lambda: [1920, 1080]) resolution_motion: list[int] = Field(default_factory=lambda: [640, 360]) zones: list["CameraZone"] = Field(default_factory=list) + location: str = "INTERIOR" # EXTERIOR | INTERIOR | TRANSITION # --- Sensor Config --- @@ -239,6 +240,48 @@ class HealthConfig(BaseModel): daily_digest_time: str = "08:00" +# --- Pet Detection Config --- + +class WildlifeThreatMap(BaseModel): + predator: list[str] = Field(default_factory=lambda: ["bear"]) + nuisance: list[str] = Field(default_factory=list) + passive: list[str] = Field(default_factory=lambda: ["bird", "horse", "cow", "sheep"]) + + +class WildlifeSizeHeuristics(BaseModel): + small: float = 0.02 # < 2% of frame → nuisance + medium: float = 0.08 # 2-8% → predator + large: float = 0.15 # > 8% → passive (deer-sized) + + +class WildlifeConfig(BaseModel): + threat_map: WildlifeThreatMap = Field(default_factory=WildlifeThreatMap) + size_heuristics: WildlifeSizeHeuristics = Field(default_factory=WildlifeSizeHeuristics) + + +class PetActivityConfig(BaseModel): + daily_digest: bool = True + highlight_clips: bool = True + zoomie_threshold: float = 0.8 + + +class PetsConfig(BaseModel): + enabled: bool = False + model: str = "yolov8s" + model_path: str = "/var/vigilar/models/yolov8s.pt" + confidence_threshold: float = 0.5 + pet_id_enabled: bool = True + pet_id_model_path: str = "/var/vigilar/models/pet_id.pt" + pet_id_threshold: float = 0.7 + pet_id_low_confidence: float = 0.5 + training_dir: str = "/var/vigilar/pets/training" + crop_staging_dir: str = "/var/vigilar/pets/staging" + crop_retention_days: int = 7 + min_training_images: int = 20 + wildlife: WildlifeConfig = Field(default_factory=WildlifeConfig) + activity: PetActivityConfig = Field(default_factory=PetActivityConfig) + + # --- Rule Config --- class RuleCondition(BaseModel): @@ -284,6 +327,7 @@ class VigilarConfig(BaseModel): detection: DetectionConfig = Field(default_factory=DetectionConfig) vehicles: VehicleConfig = Field(default_factory=VehicleConfig) health: HealthConfig = Field(default_factory=HealthConfig) + pets: PetsConfig = Field(default_factory=PetsConfig) cameras: list[CameraConfig] = Field(default_factory=list) sensors: list[SensorConfig] = Field(default_factory=list) sensor_gpio: SensorGPIOConfig = Field(default_factory=SensorGPIOConfig, alias="sensors.gpio")