Skip to content

Enhancement: Device-Aware Notification Routing #179

Description

@dannytsang

Summary

Implement intelligent device routing for notifications so messages are sent to the device a person is most likely to have with them, based on time of day, location, and device availability.

Motivation

Currently, notifications are sent to all devices simultaneously. This causes:

  • Information overload across multiple devices
  • Missed critical alerts if person isn't looking at primary device
  • Wasted battery on smartwatch notifications during work hours
  • No intelligence about device context

A smarter routing system would:

  • Reduce notification fatigue
  • Ensure messages reach the right device at the right time
  • Improve accessibility and response time
  • Adapt to user patterns and schedules

Implementation Approaches

Approach A: Context-Based (Time + Location)

Route based on time of day and person's location

Pros:

  • ✅ Simple, predictable logic
  • ✅ No external dependencies
  • ✅ Works with existing presence system
  • ✅ Easy to configure and debug
  • ✅ Good starting point

Cons:

  • ❌ Requires manual tuning per person
  • ❌ Doesn't adapt to irregular schedules
  • ❌ Doesn't track actual device availability
  • ❌ Can't handle exceptions (person working from home, sick day, etc.)

Example Logic:

120:      - if:
121:          - condition: time
122:            after: "09:00:00"
123:            before: "17:00:00"
124:          - condition: state
125:            entity_id: person.danny
126:            state: "work"
127:        then:
128:          - set: v_target_device: "notify.danny_laptop"
129:        elif:
130:          - condition: time
131:            after: "07:00:00"
132:            before: "09:00:00"
133:        then:
134:          - set: v_target_device: "notify.mobile_app_top_dog"
135:        elif:
136:          - condition: time
137:            after: "19:00:00"
138:            before: "23:00:00"
139:        then:
140:          - set: v_target_device: "notify.mobile_app_top_dog"
141:        else:
142:          - set: v_target_device: "none"  # Quiet hours
143:
144:      # Send to determined device
145:      - if:
146:          - condition: template
147:            value_template: "{{ v_target_device != 'none' }}"
148:        then:
149:          - action: "{{ v_target_device }}"
150:            data:
151:              message: "{{ message }}"

Behavior Examples:

Time Danny's Location Target Device
09:00-17:00 work laptop (notify.danny_laptop)
07:00-09:00 home phone (notify.mobile_app_top_dog)
19:00-23:00 home phone (notify.mobile_app_top_dog)
23:00-07:00 home none (quiet) or smartwatch only
14:00 away phone (not at work)

Configuration Required:

  • input_datetime for work start/end times per person
  • input_select for device preferences per time slot
  • Person's location tracking (already configured)

Approach B: Availability-Based (Device State)

Route based on actual device availability: online status, battery level, last seen

Pros:

  • ✅ Works with actual device state
  • ✅ Handles unexpected scenarios
  • ✅ Battery-aware (don't send if low battery)
  • ✅ More reliable in practice
  • ✅ Adapts to real-world device usage

Cons:

  • ❌ More complex logic (~40-50 lines)
  • ❌ Requires robust device tracking
  • ❌ Database lookups for each notification
  • ❌ Fallback logic must be well-designed
  • ❌ Depends on device_tracker accuracy

Example Logic:

120:      - if:
121:          - condition: state
122:            entity_id: device_tracker.danny_laptop
123:            state: "home"  # or specific location
124:          - condition: numeric_state
125:            entity_id: sensor.danny_laptop_battery
126:            above: 20
127:        then:
128:          - set: v_target_device: "notify.danny_laptop"
129:        elif:
130:          - condition: state
131:            entity_id: device_tracker.danny_phone
132:            state: "home"
133:          - condition: numeric_state
134:            entity_id: sensor.danny_phone_battery
135:            above: 10
136:        then:
137:          - set: v_target_device: "notify.mobile_app_top_dog"
138:        elif:
139:          - condition: state
140:            entity_id: device_tracker.danny_smartwatch
141:            state: "home"
142:          - condition: numeric_state
143:            entity_id: sensor.danny_smartwatch_battery
144:            above: 5
145:        then:
146:          - set: v_target_device: "notify.danny_smartwatch"
147:        else:
148:          - set: v_target_device: "all"  # Fallback: send to all
149:
150:      # Send to determined device
151:      - if:
152:          - condition: template
153:            value_template: "{{ v_target_device != 'none' }}"
154:        then:
155:          - action: "{{ v_target_device }}"
156:            data:
157:              message: "{{ message }}"
158:          - elif: "{{ v_target_device == 'all' }}"
159:            then:
160:              # Fallback: send to all available devices
161:              - parallel:
162:                  - action: notify.danny_laptop
163:                  - action: notify.mobile_app_top_dog
164:                  - action: notify.danny_smartwatch

Behavior Examples:

Laptop Phone Smartwatch Target Device
online, 75% battery online, 40% battery online, 12% battery laptop
offline online, 40% battery online, 12% battery phone
online, 5% battery offline online, 8% battery smartwatch
offline offline offline all (send to all available)

Configuration Required:

  • device_tracker entities for each device
  • battery sensors for each device
  • Battery threshold constants (laptop: 20%, phone: 10%, watch: 5%)
  • Location/availability status mapping

Approach C: Hybrid (Context + Availability) ⭐ Recommended

Combine time-based context routing with availability fallback for best results

Pros:

  • ✅ Smart primary routing based on context
  • ✅ Fallback to available devices if primary unavailable
  • ✅ Handles edge cases and exceptions
  • ✅ Battery-aware with intelligent fallback
  • ✅ Flexible and adaptable
  • ✅ Best of both approaches

Cons:

  • ❌ Most complex to implement (~60-80 lines)
  • ❌ Requires both time + device tracking setup
  • ❌ More maintenance and tuning needed
  • ❌ More points of failure

Example Logic:

120:      # Determine primary device based on time/context
121:      - variables:
122:          v_primary_device: null
123:          v_fallback_devices: []
124:
125:      - if:
126:          - condition: time
127:            after: "09:00:00"
128:            before: "17:00:00"
129:        then:
130:          - set: 
131:              v_primary_device: "notify.danny_laptop"
132:              v_fallback_devices: ["notify.mobile_app_top_dog", "notify.danny_smartwatch"]
133:        elif:
134:          - condition: time
135:            after: "19:00:00"
136:            before: "23:00:00"
137:        then:
138:          - set:
139:              v_primary_device: "notify.mobile_app_top_dog"
140:              v_fallback_devices: ["notify.danny_laptop", "notify.danny_smartwatch"]
141:        else:
142:          - set:
143:              v_primary_device: "notify.danny_smartwatch"
144:              v_fallback_devices: ["notify.mobile_app_top_dog"]
145:
146:      # Try primary device with availability checks
147:      - if:
148:          - condition: state
149:            entity_id: device_tracker.danny_laptop
150:            state: "home"
151:          - condition: numeric_state
152:            entity_id: sensor.danny_laptop_battery
153:            above: 20
154:        then:
155:          - action: notify.danny_laptop
156:            data:
157:              message: "{{ message }}"
158:
159:          # Fallback to secondary devices if primary unavailable
160:        else:
161:          - repeat:
162:              for_each: "{{ v_fallback_devices }}"
163:              sequence:
164:                - if:
165:                    - condition: state
166:                      entity_id: "device_tracker.danny_{{ repeat.item|regex_replace('notify.danny_(.*)','\\1') }}"
167:                      state: "home"
168:                    - condition: numeric_state
169:                      entity_id: "sensor.danny_{{ repeat.item|regex_replace('notify.danny_(.*)','\\1') }}_battery"
170:                      above: 10
171:                  then:
172:                    - action: "{{ repeat.item }}"
173:                      data:
174:                        message: "{{ message }}"

Device Priority Chain:

Work Hours (09:00-17:00)
  Primary: Laptop (battery > 20%)
    ↓
  Fallback 1: Phone (battery > 10%)
    ↓
  Fallback 2: Smartwatch (battery > 5%)
    ↓
  Fallback 3: Send to all available

Evening (19:00-23:00)
  Primary: Phone (battery > 10%)
    ↓
  Fallback 1: Laptop (battery > 20%)
    ↓
  Fallback 2: Smartwatch (battery > 5%)
    ↓
  Fallback 3: Send to all available

Night/Other
  Primary: Smartwatch (battery > 5%)
    ↓
  Fallback 1: Phone (battery > 10%)
    ↓
  Fallback 2: Send to all available

Configuration Required:

  • All items from Approach A (time-based config)
  • All items from Approach B (device tracking)
  • Device priority matrices per time period
  • Battery thresholds per device type

Behavior Example: Energy Alert at 14:30

Alert: "Solar production exceeding 3kW"
Person: Danny
Time: 14:30 (Tuesday, work hours)

Approach A (Context-Based)

  1. Check time: 14:30 → work hours
  2. Primary device: laptop
  3. Send to laptop only
  4. Danny sees alert immediately (working at laptop)

Approach B (Availability-Based)

  1. Check laptop: online, battery 75% ✅
  2. Send to laptop
  3. If offline: fallback to phone, smartwatch, or all devices
  4. Handles unexpected scenarios (working from coffee shop, forgot laptop)

Approach C (Hybrid)

  1. Check time: 14:30 → work hours
  2. Primary device: laptop
  3. Check laptop availability: online, battery 75% ✅
  4. Send to laptop
  5. If laptop offline: check phone (battery 45% ✅) → send there
  6. If both offline: smartwatch (battery 8% ✅) → send there
  7. If all offline: send to all available devices

Implementation Steps (For Future)

Phase 1: Approach A (Time-Based)

  • Define time periods per person (input_datetime helpers)
  • Create context-based routing logic
  • Add to send_direct_notification script
  • Test with different times of day
  • Document in claude.md with line numbers

Phase 2: Approach B (Availability-Based)

  • Set up device_tracker for each device
  • Create battery level sensors (if not automatic)
  • Implement availability checking logic
  • Add fallback chains
  • Test with devices offline/low battery

Phase 3: Approach C (Hybrid)

  • Combine Phase 1 + Phase 2
  • Create device priority matrices
  • Implement intelligent fallback
  • Extensive testing of edge cases
  • Performance optimization (cache device state)

Data Points Required

For Time-Based (Approach A)

input_datetime:
  danny_work_start:
    name: "Danny Work Start Time"
    initial: "09:00:00"
  danny_work_end:
    name: "Danny Work End Time"
    initial: "17:00:00"
  danny_morning_start:
    name: "Danny Morning Time"
    initial: "07:00:00"
  danny_evening_start:
    name: "Danny Evening Time"
    initial: "19:00:00"
  danny_night_end:
    name: "Danny Night End Time"
    initial: "23:00:00"

input_select:
  danny_work_hours_device:
    name: "Danny Work Hours Device"
    options:
      - "laptop"
      - "phone"
      - "smartwatch"
    initial: "laptop"

For Availability-Based (Approach B)

device_tracker:
  danny_laptop:
    name: "Danny Laptop"
    unique_id: danny_laptop_tracker
    
  danny_phone:
    name: "Danny Phone"
    unique_id: danny_phone_tracker

sensor:
  danny_laptop_battery:
    name: "Danny Laptop Battery"
    unit_of_measurement: "%"
    
  danny_phone_battery:
    name: "Danny Phone Battery"
    unit_of_measurement: "%"

Related Issues & PRs

Acceptance Criteria

  • Device routing logic implemented for chosen approach
  • Tested with all time periods
  • Tested with devices offline/low battery
  • Fallback behavior working correctly
  • Configuration documented
  • Added to claude.md with line numbers
  • No notifications lost in routing

Labels

enhancement, notifications, phase-3, device-routing

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions