Per-pet configurable rules with conditions (zone, time, presence) and actions (push, log, record). Walk tracker, missing pet, on-the-loose detection via composable rule builder UI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
15 KiB
Pet Lifestyle Rules Engine — Design Spec
Overview
A configurable per-pet rules engine that evaluates conditions against the detection event stream and triggers actions. Replaces hardcoded pet alert logic with a general-purpose system. Supports up to 8 pets with up to 32 rules each. Covers walk tracking, outdoor timers, missing pet alerts, "on the loose" detection, and any other condition-action pattern a user can compose.
Example Rules
- "Alert if Milo hasn't been seen outside in 4 hours" (needs to go out)
- "Alert if Milo has been outside for 45 minutes" (overdue from walk)
- "Alert if Snake is detected without a person present" (on the loose)
- "Alert if Taquito hasn't been seen anywhere in 8 hours" (missing)
- "Alert if Angel is outside at night" (wrong zone + time)
Rule Structure
Every rule has three parts:
Trigger + Logic (Conditions)
Conditions are evaluated together with AND logic (all must be true for the rule to fire).
| Condition Type | Parameters | Description |
|---|---|---|
detected_in_zone |
zone: EXTERIOR, INTERIOR, TRANSITION |
Pet detected in a specific zone type |
not_seen_in_zone |
zone, minutes |
Pet NOT detected in a zone for N minutes |
not_seen_anywhere |
minutes |
Pet not detected on any camera for N minutes |
detected_without_person |
— | Pet detected but no person on same camera within 30 seconds |
in_zone_longer_than |
zone, minutes |
Pet continuously in a zone for N minutes |
time_of_day |
start, end (HH:MM) |
Current time within a window (handles midnight wrap) |
Conditions combine naturally:
detected_in_zone(EXTERIOR) AND time_of_day(22:00-06:00)= pet outside at nightdetected_without_person AND detected_in_zone(INTERIOR)= snake loose indoorsnot_seen_in_zone(EXTERIOR) AND time_of_day(08:00-20:00)= dog hasn't gone out during daytime
Actions
| Action | Description |
|---|---|
push_notify |
Send a push notification with configurable message template |
log_event |
Write to event log without notification |
start_recording |
Trigger recording on the relevant camera |
Action messages support placeholders: {pet_name}, {camera}, {zone}, {duration}, {last_seen}.
Example: "{pet_name} has been outside for {duration} — {camera}" → "Milo has been outside for 47 minutes — Back Deck"
Cooldown
Each rule has a cooldown period (configurable, default 30 minutes). Once a rule fires, it won't fire again until the cooldown expires. Prevents notification spam.
Rule Evaluation Engine
New File: vigilar/pets/rules.py
PetRuleEngine class:
Per-pet state tracking:
@dataclass
class PetState:
pet_id: str
last_seen_camera: str | None = None
last_seen_time: float = 0
current_zone: str | None = None # EXTERIOR, INTERIOR, TRANSITION
zone_entry_time: float = 0 # when they entered current zone
person_present: bool = False # person detected on same camera recently
person_last_seen: float = 0 # timestamp of last person on same camera
Event-triggered evaluation:
- Subscribes to
vigilar/camera/+/pet/detectedandvigilar/camera/+/motion/start(for person detection) via MQTT - On each pet detection event: update PetState, evaluate all enabled rules for that pet
- On each person detection: update
person_presentstate for the camera person_presentis True if a person was detected on the same camera within the last 30 seconds
Timer-triggered evaluation:
- A background timer runs every 60 seconds
- Evaluates time-based conditions (
not_seen_in_zone,not_seen_anywhere,in_zone_longer_than) for all pets with active rules - Only evaluates rules with at least one time-based condition
Deduplication:
- Tracks
last_firedtimestamp per rule ID - Rule won't fire again until
now - last_fired >= cooldown_minutes * 60
Condition Evaluation
def evaluate_rule(rule, pet_state, now) -> bool:
for condition in rule.conditions:
if not evaluate_condition(condition, pet_state, now):
return False
return True
def evaluate_condition(condition, pet_state, now) -> bool:
match condition["type"]:
case "detected_in_zone":
return pet_state.current_zone == condition["zone"]
case "not_seen_in_zone":
# True if pet hasn't been seen in the specified zone for N minutes
if pet_state.current_zone == condition["zone"]:
return False # they're there right now
minutes = condition["minutes"]
return (now - pet_state.last_seen_time) >= minutes * 60
case "not_seen_anywhere":
minutes = condition["minutes"]
return (now - pet_state.last_seen_time) >= minutes * 60
case "detected_without_person":
return not pet_state.person_present
case "in_zone_longer_than":
if pet_state.current_zone != condition["zone"]:
return False
minutes = condition["minutes"]
return (now - pet_state.zone_entry_time) >= minutes * 60
case "time_of_day":
current_time = time.strftime("%H:%M")
return is_in_time_window(condition["start"] + "-" + condition["end"], current_time)
Uses the existing is_in_time_window from vigilar/alerts/profiles.py for time window evaluation (handles midnight wraparound).
Action Execution
When a rule fires:
- Format the action message with placeholders resolved from PetState
- Execute the action:
push_notify: publish toTopics.SYSTEM_ALERTwith the formatted message (picked up by the notification sender from Group A)log_event: insert an event with typePET_RULE_TRIGGEREDand the rule details in the payloadstart_recording: publish a record command to the relevant camera
- Update
last_firedfor the rule - Log to
alert_logtable
New Event Types
Add to EventType enum:
PET_RULE_TRIGGERED— a pet rule fired
Database
New Table: pet_rules
| Column | Type | Notes |
|---|---|---|
| id | Integer PK | Autoincrement |
| pet_id | String NOT NULL | FK to pets table |
| name | String NOT NULL | User-given label (e.g., "Outdoor timer") |
| enabled | Integer NOT NULL | 1 = active, 0 = disabled |
| conditions | Text NOT NULL | JSON array of condition objects |
| action | String NOT NULL | push_notify, log_event, start_recording |
| action_message | String | Template with {placeholders} |
| cooldown_minutes | Integer NOT NULL | Default 30 |
| priority | Integer NOT NULL | Evaluation order (lower = first) |
| created_at | Float NOT NULL | Timestamp |
Index: idx_pet_rules_pet on (pet_id, enabled, priority)
Condition JSON Format
[
{"type": "detected_in_zone", "zone": "EXTERIOR"},
{"type": "time_of_day", "start": "22:00", "end": "06:00"}
]
[
{"type": "not_seen_in_zone", "zone": "EXTERIOR", "minutes": 240}
]
[
{"type": "detected_without_person"},
{"type": "detected_in_zone", "zone": "INTERIOR"}
]
Query Functions
Add to vigilar/storage/queries.py:
get_pet_rules(engine, pet_id) -> list[dict]get_all_enabled_rules(engine) -> list[dict]insert_pet_rule(engine, pet_id, name, conditions, action, action_message, cooldown_minutes, priority) -> intupdate_pet_rule(engine, rule_id, **updates) -> Nonedelete_pet_rule(engine, rule_id) -> Nonecount_pet_rules(engine, pet_id) -> int(for enforcing 32-rule cap)
Configuration
[pets]
max_pets = 8
max_rules_per_pet = 32
rule_eval_interval_s = 60 # how often to check time-based conditions
Web Routes
| Route | Method | Purpose |
|---|---|---|
/pets/<pet_id>/rules |
GET | List rules for a pet (JSON) |
/pets/<pet_id>/rules |
POST | Create a new rule |
/pets/<pet_id>/rules/<rule_id> |
PUT | Update a rule |
/pets/<pet_id>/rules/<rule_id> |
DELETE | Delete a rule |
/pets/api/rule-templates |
GET | List available quick-add templates |
Rule creation validation
- Verify pet_id exists
- Verify rule count <
max_rules_per_pet - Validate condition JSON: each condition has a valid
type, required parameters present - Validate action is one of the allowed values
- Validate cooldown_minutes >= 1
Rule Builder UI
Layout
New "Rules" tab on each pet's section of the pet dashboard. Rules displayed as a vertical list of cards, each showing the rule in natural language form.
Rule Card Display
Each rule card renders its conditions and action in readable form:
🟢 Outdoor Timer [Edit] [Delete] [Toggle]
WHEN Milo is in EXTERIOR longer than 45 minutes
THEN Push notification: "Milo has been outside for {duration}"
Cooldown: 30 minutes
🟢 Needs to go out [Edit] [Delete] [Toggle]
WHEN Milo not seen in EXTERIOR for 240 minutes
AND time is between 08:00 and 20:00
THEN Push notification: "Milo hasn't been outside in {duration}"
Cooldown: 60 minutes
🟢 Snake on the loose [Edit] [Delete] [Toggle]
WHEN Snek is detected without a person present
THEN Push notification: "Snake spotted without supervision — {camera}"
Cooldown: 5 minutes
Rule Editor
When creating or editing a rule, the form renders as a series of logic steps:
Step 1 — Conditions (WHEN/AND):
Each condition is a form row with dropdowns:
WHEN [condition type ▼] [parameters...]
┌─────────────────────────────────┐
│ detected in zone │
│ not seen in zone for │
│ not seen anywhere for │
│ detected without person │
│ in zone longer than │
│ time of day │
└─────────────────────────────────┘
Selecting a condition type reveals its parameter fields:
detected_in_zone→ zone dropdown (EXTERIOR / INTERIOR / TRANSITION)not_seen_in_zone→ zone dropdown + minutes inputnot_seen_anywhere→ minutes inputdetected_without_person→ no parametersin_zone_longer_than→ zone dropdown + minutes inputtime_of_day→ start time + end time inputs
"+ Add condition" button adds an AND row (up to 4 conditions per rule).
Step 2 — Action (THEN):
THEN [action type ▼] [message template]
┌─────────────────────────────────┐
│ Push notification │
│ Log event only │
│ Start recording │
└─────────────────────────────────┘
Message template text input with placeholder hints shown below: Available: {pet_name}, {camera}, {zone}, {duration}, {last_seen}
Step 3 — Cooldown:
Cooldown: [30] minutes
Quick-Add Templates
"Quick Add" button opens a template picker modal:
| Template | Pre-filled Conditions | Pre-filled Action |
|---|---|---|
| Outdoor timer | in_zone_longer_than(EXTERIOR, 45) |
"🐕 {pet_name} has been outside for {duration}" |
| Needs to go out | not_seen_in_zone(EXTERIOR, 240) |
"🐕 {pet_name} hasn't been outside in {duration}" |
| Missing pet | not_seen_anywhere(480) |
"❓ {pet_name} hasn't been seen in {duration}" |
| Wrong zone alert | detected_in_zone(EXTERIOR) |
"⚠️ {pet_name} detected in {zone} — {camera}" |
| On the loose | detected_without_person |
"🐍 {pet_name} spotted without supervision — {camera}" |
| Night escape | detected_in_zone(EXTERIOR), time_of_day(22:00-06:00) |
"🌙 {pet_name} is outside at night — {camera}" |
User picks a template, it pre-fills the form, they adjust values and save. Templates are a convenience — every rule is editable after creation.
Process Integration
PetRuleEngine as a Subprocess
The rule engine runs in the event processor's process (not a separate subprocess). It:
- Loads all enabled rules from DB at startup
- Subscribes to pet detection and person detection MQTT topics
- Maintains in-memory PetState per pet
- Evaluates event-triggered rules on each detection
- Runs a 60-second timer for time-based rule evaluation
- Reloads rules from DB when it receives a
vigilar/system/rules_updatedMQTT message (published by the web blueprint when rules are created/updated/deleted)
MQTT Topics
New topic for rule reload signal:
vigilar/system/rules_updated — published when rules change, engine reloads from DB
Migration from PET_ESCAPE
The existing PET_ESCAPE event generation in events/processor.py stays as-is for backward compatibility with alert profiles. Pet rules operate as an additional layer — they can coexist. Users who want the old behavior don't need to create rules. Users who want more control create rules that supersede the built-in behavior.
If a user creates a detected_in_zone(EXTERIOR) rule for a pet, they'll get both the PET_ESCAPE event (from the processor) and the rule notification. This is acceptable — the cooldown prevents spam, and the user can disable the alert profile rule for known_pet if they prefer the rules engine.
Dependencies
New Packages
None.
New Files
| File | Purpose |
|---|---|
vigilar/pets/__init__.py |
Package init |
vigilar/pets/rules.py |
PetRuleEngine, PetState, condition evaluation |
vigilar/web/templates/pets/rules.html |
Rule builder UI (partial template included in dashboard) |
Modified Files
| File | Changes |
|---|---|
vigilar/constants.py |
Add PET_RULE_TRIGGERED to EventType |
vigilar/config.py |
Add max_pets, max_rules_per_pet, rule_eval_interval_s to PetsConfig |
vigilar/storage/schema.py |
Add pet_rules table |
vigilar/storage/queries.py |
Add rule CRUD functions |
vigilar/events/processor.py |
Initialize and run PetRuleEngine |
vigilar/web/blueprints/pets.py |
Add rule CRUD routes + templates route |
vigilar/web/templates/pets/dashboard.html |
Add Rules tab per pet |
Out of Scope
- Drag-and-drop flowchart builder (v2 — start with the vertical list form builder which is simpler and mobile-friendly)
- OR logic between conditions (AND only for v1 — covers all identified use cases)
- Rule chaining (one rule triggering another)
- Per-camera rules (rules are per-pet, not per-camera — the camera is determined by where the detection happens)
- Rule import/export
- Rule sharing between pets (create separately per pet)