vigilar/docs/superpowers/specs/2026-04-03-pet-lifestyle-design.md
Aaron D. Lee f530f26530 Add pet lifestyle rules engine design spec (S5)
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>
2026-04-03 15:27:48 -04:00

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 night
  • detected_without_person AND detected_in_zone(INTERIOR) = snake loose indoors
  • not_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/detected and vigilar/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_present state for the camera
  • person_present is 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_fired timestamp 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:

  1. Format the action message with placeholders resolved from PetState
  2. Execute the action:
    • push_notify: publish to Topics.SYSTEM_ALERT with the formatted message (picked up by the notification sender from Group A)
    • log_event: insert an event with type PET_RULE_TRIGGERED and the rule details in the payload
    • start_recording: publish a record command to the relevant camera
  3. Update last_fired for the rule
  4. Log to alert_log table

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) -> int
  • update_pet_rule(engine, rule_id, **updates) -> None
  • delete_pet_rule(engine, rule_id) -> None
  • count_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 input
  • not_seen_anywhere → minutes input
  • detected_without_person → no parameters
  • in_zone_longer_than → zone dropdown + minutes input
  • time_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:

  1. Loads all enabled rules from DB at startup
  2. Subscribes to pet detection and person detection MQTT topics
  3. Maintains in-memory PetState per pet
  4. Evaluates event-triggered rules on each detection
  5. Runs a 60-second timer for time-based rule evaluation
  6. Reloads rules from DB when it receives a vigilar/system/rules_updated MQTT 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)