[ Home Assistant: Simple DIY Alarm System ]

← back to Home Automation
[ Overview | Step 1 — Panel | Step 2 — Detection | Step 3 — Response | Step 4 — RGB feedback | Step 5 — PTZ tracking | Step 6 — Privacy mode | Optional | Caveats ]

[ Overview ]

A simple, self-contained DIY alarm system built on top of Home Assistant.
Uses whatever contact + motion sensors you already have, an
alarm_control_panel entity for arm/disarm state, and TTS + flashing
lights as the response when something trips.

Three modes, distinct trigger sets:

  armed_home   perimeter only (you're inside, motion is fine)
  armed_night  perimeter only, interior doors excluded
                so you can pee at 3am without setting it off
  armed_away   everything — perimeter, interior, and motion

The example below assumes a small set of sensors (front + rear contact,
one bedroom contact, one kitchen motion) but the pattern scales to any
number of sensors — just append entity_ids to the right trigger lists.

Example sensor map

  Sensor          Entity                                            Type     Location
  ─────────────────────────────────────────────────────────────────────────────────────
  Front Door      binary_sensor.front_door_door                     contact  Living Room
  Rear/Sliding    binary_sensor.rear_door_door                      contact  Living Room
  Bedroom Door    binary_sensor.bedroom_door_door                   contact  Bedroom (interior)
  Kitchen Motion  binary_sensor.camera_e1_5295_motion_sensor        motion   Kitchen

The bedroom door is interior, so it's excluded from armed_night — otherwise a
2am bathroom trip would trigger the siren on yourself.

[ Step 1 — The Alarm Panel ]

Drop this in configuration.yaml. It creates the
alarm_control_panel.home_alarm entity that everything else hangs off
of. After saving, restart Home Assistant.
# configuration.yaml
alarm_control_panel:
  - platform: manual
    name: Home Alarm
    code: !secret alarm_code            # set this in secrets.yaml, e.g. alarm_code: "1234"
    code_arm_required: false            # don't require code to arm, only to disarm
    arming_time: 30                     # 30s grace period to leave after arming
    delay_time: 20                      # 20s entry delay before triggering
    trigger_time: 600                   # alarm sirens for 10 minutes
    disarmed:
      trigger_time: 0
    armed_home:
      arming_time: 0                    # arming home = instant, you're already there
      delay_time: 0                     # no entry delay in home mode (instant alarm)
    armed_night:
      arming_time: 0
      delay_time: 20
    armed_away:
      arming_time: 30
      delay_time: 20
After restart you'll have alarm_control_panel.home_alarm as a state
machine, plus a Lovelace card you can drop on a dashboard for arm/disarm
buttons. The arm code is enforced on disarm only — arming with a single
tap, disarm requires the PIN. Adjust to taste.

[ Step 2 — Detection automations ]

Three automations watch the sensors. Each one fires
alarm_control_panel.alarm_trigger when its conditions are met —
the panel itself handles the entry-delay countdown and state machine.

Place these in automations.yaml (or wherever you keep
automations).
# automations.yaml

# Perimeter trigger — fires in ALL armed modes
- alias: "Alarm: Perimeter breach"
  description: "Front or rear door opened while alarm is armed"
  trigger:
    - platform: state
      entity_id:
        - binary_sensor.front_door_door
        - binary_sensor.rear_door_door
      to: "on"
  condition:
    - condition: state
      entity_id: alarm_control_panel.home_alarm
      state:
        - armed_home
        - armed_away
        - armed_night
  action:
    - action: alarm_control_panel.alarm_trigger
      target:
        entity_id: alarm_control_panel.home_alarm

# Interior trigger — only in away mode (home/night = you're inside)
- alias: "Alarm: Interior motion"
  description: "Kitchen motion while armed away"
  trigger:
    - platform: state
      entity_id: binary_sensor.camera_e1_5295_motion_sensor
      to: "on"
  condition:
    - condition: state
      entity_id: alarm_control_panel.home_alarm
      state: armed_away
  action:
    - action: alarm_control_panel.alarm_trigger
      target:
        entity_id: alarm_control_panel.home_alarm

# Bedroom door — only in away mode (otherwise the bathroom trip rule)
- alias: "Alarm: Bedroom door (away only)"
  trigger:
    - platform: state
      entity_id: binary_sensor.bedroom_door_door
      to: "on"
  condition:
    - condition: state
      entity_id: alarm_control_panel.home_alarm
      state: armed_away
  action:
    - action: alarm_control_panel.alarm_trigger
      target:
        entity_id: alarm_control_panel.home_alarm

[ Step 3 — Response automations ]

When the panel transitions states, these fire the actual response — entry
warning, full alarm, and a disarm confirmation. The pattern is to watch
state changes on the panel itself, not on the sensors, so you don't
have to duplicate "is it armed?" logic everywhere.
# When the alarm enters "pending" (entry delay countdown), warn first
- alias: "Alarm: Entry warning"
  trigger:
    - platform: state
      entity_id: alarm_control_panel.home_alarm
      to: pending
  action:
    - action: media_player.play_media
      target:
        entity_id: media_player.nest_display
      data:
        media_content_id: >-
          media-source://tts/tts.piper?message=Alarm+will+trigger+in+twenty+seconds.+Disarm+now.&language=en_US
        media_content_type: audio/mp3

# When the alarm actually triggers — full response
- alias: "Alarm: Triggered response"
  trigger:
    - platform: state
      entity_id: alarm_control_panel.home_alarm
      to: triggered
  action:
    # 1. Push notification with high priority
    - action: notify.mobile_app_magikh0e_phone
      data:
        title: "🚨 ALARM TRIGGERED"
        message: "Security system has been triggered at home."
        data:
          priority: high
          ttl: 0
    # 2. Loud TTS on every available speaker
    - action: media_player.volume_set
      target:
        entity_id:
          - media_player.nest_display
          - media_player.m5stack_atom_echo_a17a48
      data:
        volume_level: 1.0
    - action: media_player.play_media
      target:
        entity_id:
          - media_player.nest_display
          - media_player.m5stack_atom_echo_a17a48
      data:
        media_content_id: >-
          media-source://tts/tts.piper?message=Intruder+Alert!&language=en_US
        media_content_type: audio/mp3
    # 3. After TTS finishes, cast a YouTube video to the Nest display
    #    (Google Cast handles video/youtube content type natively).
    - delay: "00:00:03"
    - action: media_player.play_media
      target:
        entity_id: media_player.nest_display
      data:
        media_content_id: https://www.youtube.com/watch?v=04F4xlWSFh0
        media_content_type: video/youtube
    # 4. Flash all lights red as a deterrent + visibility
    - repeat:
        count: 30
        sequence:
          - action: light.turn_on
            target:
              entity_id:
                - light.living_room_lights
                - light.bedroom_lights
            data:
              brightness: 255
              rgb_color: [255, 0, 0]
          - delay: "00:00:01"
          - action: light.turn_off
            target:
              entity_id:
                - light.living_room_lights
                - light.bedroom_lights
          - delay: "00:00:01"

# When disarmed, send confirmation
- alias: "Alarm: Disarmed"
  trigger:
    - platform: state
      entity_id: alarm_control_panel.home_alarm
      to: disarmed
  action:
    - action: notify.mobile_app_magikh0e_phone
      data:
        message: "Alarm disarmed at {{ now().strftime('%I:%M %p') }}"

[ Step 4 — RGB light feedback ]

Visual feedback for state changes — bulbs flash blue when you arm,
green when you disarm, breathe amber during the entry-delay countdown,
and the existing red strobe handles intruder alerts. Makes the alarm
state obvious from across the room without staring at a dashboard.

First, group your RGB bulbs into a single light.alarm_lights
entity so the automations target one thing instead of repeating an
entity_id list everywhere. Add this to configuration.yaml
alongside the alarm_control_panel block from Step 1.
# configuration.yaml
light:
  - platform: group
    name: Alarm Lights
    entities:
      - light.living_room_lights
      - light.bedroom_lights
      - light.kitchen_lights
After restart you'll have light.alarm_lights covering every
bulb. Now the feedback automations — drop these in
automations.yaml.
# automations.yaml — RGB light feedback

# Armed → quick double blue flash to confirm the arm action.
# Fires for any of the three armed states so you get the same visual
# regardless of how you armed (manual / auto-arm / presence-based).
- alias: "Alarm: Light feedback — armed"
  mode: restart
  trigger:
    - platform: state
      entity_id: alarm_control_panel.home_alarm
      to:
        - armed_home
        - armed_night
        - armed_away
  action:
    # Snapshot current light state so we can restore it after the flash
    - action: scene.create
      data:
        scene_id: alarm_lights_before
        snapshot_entities: light.alarm_lights
    - repeat:
        count: 2
        sequence:
          - action: light.turn_on
            target:
              entity_id: light.alarm_lights
            data:
              brightness: 200
              rgb_color: [0, 100, 255]    # blue
              transition: 0
          - delay: "00:00:00.4"
          - action: light.turn_off
            target:
              entity_id: light.alarm_lights
            data:
              transition: 0
          - delay: "00:00:00.4"
    # Restore whatever the lights were doing before
    - action: scene.turn_on
      target:
        entity_id: scene.alarm_lights_before

# Disarmed → single green pulse confirming "all clear"
- alias: "Alarm: Light feedback — disarmed"
  mode: restart
  trigger:
    - platform: state
      entity_id: alarm_control_panel.home_alarm
      to: disarmed
  action:
    - action: scene.create
      data:
        scene_id: alarm_lights_before
        snapshot_entities: light.alarm_lights
    - action: light.turn_on
      target:
        entity_id: light.alarm_lights
      data:
        brightness: 200
        rgb_color: [50, 255, 80]          # green
        transition: 0
    - delay: "00:00:01.5"
    - action: scene.turn_on
      target:
        entity_id: scene.alarm_lights_before

# Pending (entry-delay countdown) → amber breathing pulse
# Stops automatically when the panel leaves "pending" — either you
# disarmed in time, or the alarm tripped and Step 3's red flash takes
# over. mode: restart so a fresh entry resets cleanly.
- alias: "Alarm: Light feedback — entry delay"
  mode: restart
  trigger:
    - platform: state
      entity_id: alarm_control_panel.home_alarm
      to: pending
  action:
    - action: scene.create
      data:
        scene_id: alarm_lights_before
        snapshot_entities: light.alarm_lights
    - repeat:
        while:
          - condition: state
            entity_id: alarm_control_panel.home_alarm
            state: pending
        sequence:
          - action: light.turn_on
            target:
              entity_id: light.alarm_lights
            data:
              brightness: 255
              rgb_color: [255, 140, 0]    # amber
              transition: 0
          - delay: "00:00:00.6"
          - action: light.turn_off
            target:
              entity_id: light.alarm_lights
            data:
              transition: 0
          - delay: "00:00:00.6"
    # Restore prior state if we exited because of disarm
    # (if we exited because of trigger, Step 3 takes over and
    # this scene.turn_on is harmlessly overwritten by the red flash)
    - action: scene.turn_on
      target:
        entity_id: scene.alarm_lights_before
Color cheat sheet

  blue     [0, 100, 255]    armed (any mode)
  green    [50, 255, 80]    disarmed / all clear
  amber    [255, 140, 0]    pending (entry delay countdown)
  red      [255, 0, 0]      triggered (Step 3, already wired)

The scene.create → flash → scene.turn_on
sandwich is the canonical "pulse without disrupting" pattern in HA.
Without it, every arm/disarm would turn off whatever lights you had
on. With it, your reading lamp keeps its warm-2700K glow once the
blue confirmation flash finishes.

Refactoring tip
The Step 3 red-flash response still uses
light.living_room_lights and light.bedroom_lights
directly. Now that light.alarm_lights exists, you can swap
those entity_ids for the group and lose a few lines.

[ Step 5 — PTZ camera + presence-driven tracking ]

A pan/tilt/zoom camera turns "something tripped the motion sensor" into
"I have a face on file." When a presence or motion sensor fires, the
camera pivots to that zone's preset, settles, snaps a still, and sends
the image to your phone alongside the alert push.

This pairs naturally with the alarm: while armed_away, every
zone trip captures evidence; while disarmed, the camera stays
quiet so it isn't snapping you reading on the couch.

One-time setup — store presets on the camera

Use the camera's web UI (or a tool like ONVIF Device Manager) to drive
the gimbal to each zone in turn and save it as a numbered preset.
Cameras typically support 8–255 preset slots; small cameras
like a Reolink E1 hold 64. The token you'll reference in YAML is
just the preset slot number as a string.

  Preset 1 → Living Room
  Preset 2 → Kitchen
  Preset 3 → Front Entry
  Preset 4 → Hallway
  Preset 0 → "home" / idle position (camera returns here
                  after a few minutes of quiet)

Motion vs. presence sensors

  PIR motion (Aqara P1, Hue, Zigbee multi-sensors) —
  cheap, battery-powered, fires on initial movement then has a
  cooldown. Misses a stationary intruder. Best for hallways and
  doorways where the target moves through.

  mmWave presence (Aqara FP2, LD2410/LD2412 ESPHome boards,
  Apollo MSR, Everything Presence One) — radar-based,
  detects breathing/sub-cm motion. Holds "on" while
  someone's in the room even if they sit still. Best for rooms
  where dwell time matters. Mostly mains-powered.

  Both expose binary_sensor.<name>_presence or
  _occupancy — the YAML pattern below works the same
  for either.

Example sensor-to-zone map

  Sensor                                      Zone           Preset
  ─────────────────────────────────────────────────────────────────
  binary_sensor.living_room_presence          Living Room    1
  binary_sensor.kitchen_motion                Kitchen        2
  binary_sensor.front_door_door               Front Entry    3
  binary_sensor.hallway_presence              Hallway        4
The DRY version — one automation, a lookup table mapping each
sensor to its preset + label, and a single snap/notify action chain
that runs regardless of which sensor tripped. Adding a new zone is a
two-line edit to the table, not a new automation.
# automations.yaml — PTZ tracking

- alias: "PTZ: Track motion to zone"
  description: "When armed_away, pan to the tripped zone, snap, and notify"
  mode: queued        # queue triggers so rapid sequential motion doesn't lose snapshots
  max: 5
  trigger:
    - platform: state
      entity_id:
        - binary_sensor.living_room_presence
        - binary_sensor.kitchen_motion
        - binary_sensor.front_door_door
        - binary_sensor.hallway_presence
      to: "on"
  condition:
    - condition: state
      entity_id: alarm_control_panel.home_alarm
      state: armed_away
  variables:
    # Lookup table: sensor entity_id → { preset_token, human label }
    zones:
      binary_sensor.living_room_presence: { preset: "1", label: "Living Room" }
      binary_sensor.kitchen_motion:       { preset: "2", label: "Kitchen" }
      binary_sensor.front_door_door:      { preset: "3", label: "Front Entry" }
      binary_sensor.hallway_presence:     { preset: "4", label: "Hallway" }
    zone: "{{ zones[trigger.entity_id] }}"
    stamp: "{{ now().strftime('%Y%m%d_%H%M%S') }}"
    file_path: "/config/www/snapshots/ptz_{{ stamp }}.jpg"
    web_path: "/local/snapshots/ptz_{{ stamp }}.jpg"
  action:
    # 1. Move the gimbal to the zone preset
    - action: onvif.ptz
      target:
        entity_id: camera.ptz_main
      data:
        preset_token: "{{ zone.preset }}"

    # 2. Wait for the camera to settle and re-focus
    - delay: "00:00:03"

    # 3. Capture a still
    - action: camera.snapshot
      target:
        entity_id: camera.ptz_main
      data:
        filename: "{{ file_path }}"

    # 4. Push notification with snapshot attached
    - action: notify.mobile_app_magikh0e_phone
      data:
        title: "🎥 Motion: {{ zone.label }}"
        message: "{{ now().strftime('%I:%M %p') }} — snapshot captured"
        data:
          image: "{{ web_path }}"
          ttl: 0
          priority: high
Return-to-home

After a flurry of zone-trips the camera ends up parked at whichever
preset fired last. A separate automation parks it back at preset 0
(or wherever you like to leave it) once everything has been quiet
for a while.
# automations.yaml — PTZ idle return

- alias: "PTZ: Return to home"
  description: "Park the gimbal at the home preset after 5 min of no zone trips"
  trigger:
    - platform: state
      entity_id:
        - binary_sensor.living_room_presence
        - binary_sensor.kitchen_motion
        - binary_sensor.front_door_door
        - binary_sensor.hallway_presence
      to: "off"
      for: "00:05:00"
  condition:
    # Only re-park if every zone is currently quiet
    - condition: state
      entity_id: binary_sensor.living_room_presence
      state: "off"
    - condition: state
      entity_id: binary_sensor.kitchen_motion
      state: "off"
    - condition: state
      entity_id: binary_sensor.front_door_door
      state: "off"
    - condition: state
      entity_id: binary_sensor.hallway_presence
      state: "off"
  action:
    - action: onvif.ptz
      target:
        entity_id: camera.ptz_main
      data:
        preset_token: "0"
Bonus — attach a snapshot to the main alarm trigger

The triggered-response automation in Step 3 already pushes a notification
when something fires the alarm. With PTZ wired up, you can chain a
snapshot of whichever zone tripped first into that push so the alert
that wakes you up has a face attached. Insert this between the existing
"Push notification" step and the TTS step:
    # 1b. Snapshot whichever zone tripped most recently
    - action: camera.snapshot
      target:
        entity_id: camera.ptz_main
      data:
        filename: "/config/www/snapshots/alarm_{{ now().strftime('%Y%m%d_%H%M%S') }}.jpg"
    - action: notify.mobile_app_magikh0e_phone
      data:
        title: "🚨 ALARM TRIGGERED"
        message: "Snapshot from PTZ — {{ now().strftime('%I:%M %p') }}"
        data:
          image: "/local/snapshots/alarm_{{ now().strftime('%Y%m%d_%H%M%S') }}.jpg"
          priority: high
          ttl: 0
PTZ caveats

  1. Mechanical wear.
     The gimbal motors aren't rated for thousands of trips per day.
     The queued / max: 5 mode on the tracking automation is a
     soft limit; if a zone fires every minute, consider a
     throttle condition (e.g., "not within 30s of last
     run") to keep the motors alive.

  2. Settling time.
     The 3-second delay before the snapshot is the camera's pan
     speed plus its autofocus settle time. Fast PTZs (Reolink RLC-823A)
     can do ~1.5s; slower gimbals (Hikvision IP Speed Domes) need 4–5s.
     Tune to your specific camera.

  3. Snapshot path.
     /config/www/ is HA's web-exposed directory — files
     dropped there are reachable at /local/<path> from
     the LAN, which is what the mobile app fetches the notification
     image from. Different paths break the attached-image flow.

  4. Storage.
     One snapshot per zone-trip adds up. Pair this with a daily
     automation that wipes /config/www/snapshots/ entries
     older than N days, or pipe to S3 / a NAS share if you want
     to keep evidence longer.

  5. mmWave false positives.
     Ceiling fans, HVAC vents, water vapor from showers, and curtain
     movement can all trigger 24GHz radar. Place sensors thoughtfully
     and use the per-sensor sensitivity sliders. PIR is more
     selective by nature but pays for it with dwell-time blindness.

[ Step 6 — Privacy mode (camera off when home) ]

Step 5 wakes the camera up when the alarm is armed and someone trips a
zone. Step 6 is the inverse: as soon as anyone is home, the
camera goes into a privacy mode that keeps it from filming you making
coffee, walking to the bathroom, or reading on the couch.

Two signals decide whether someone's home:

  Phone presence — the Home Assistant companion app
  publishes person.<you> with state home /
  not_home based on phone GPS + your defined Home zone. Combine
  multiple device_tracker.* entities into a single
  person.* for redundancy (phone + watch + laptop — if
  any of them is home, you're home).

  Indoor presence sensors — the same mmWave / PIR sensors
  driving Step 5's tracking. Useful for guests, kids, and roommates
  who don't have HA on their phone. Also catches the case where your
  phone died but you're physically inside.

A template binary_sensor folds both into a single
binary_sensor.someone_home that the privacy automations watch.
The delay_off grace period prevents the sensor from flickering
off every time a presence sensor cools down between motion events.

What "privacy mode" means in YAML

Privacy mode is a stack — layer as many of these as your hardware
supports, because each one alone has a failure mode the others cover:

  1. PTZ to a wall-facing preset (do this always).
     Save a preset (e.g., slot 99) where the gimbal physically points
     at a blank wall, the ceiling, or into a sock. For cameras
     without a hardware shutter, this preset IS your privacy
     mechanism — the lens still sees, but it sees nothing
     useful. Pick a wall, not a ceiling, if your wall is closer than
     the camera's minimum focus distance — out-of-focus blur
     is bonus privacy.

  2. Hardware shutter (if your camera has one).
     Eufy indoor cams, Reolink E1 Outdoor, and a few others have a
     motorized lens cover. Most integrations expose this as
     switch.<cam>_privacy_mode. This is the only
     privacy mechanism that's cryptographically certain — the
     lens is physically blind regardless of firmware state.

  3. Stop recording at the camera.
     Vendor switches like switch.<cam>_record,
     _motion_detection, _record_audio,
     _ftp_upload, _email. Toggling these stops the
     camera from persisting clips locally or shipping them to a
     cloud you don't want them on.

  4. Stop the stream into HA.
     camera.turn_off tells the integration to stop pulling
     RTSP from the camera. Kills any HA-side recording (Frigate,
     LLAT, NVR add-on) without touching the camera itself.

  5. Disable the Step 5 tracking automation.
     Belt-and-suspenders: even if a frame somehow leaked through,
     no automation will snap it and push it to your phone.

The example below stacks all five.
# configuration.yaml — composite "someone is home" sensor

template:
  - binary_sensor:
      - name: "Someone Home"
        unique_id: someone_home
        # ON when ANY tracker says home OR ANY presence sensor is active
        state: >-
          {{ is_state('person.magikh0e', 'home')
             or is_state('person.partner', 'home')
             or is_state('binary_sensor.living_room_presence', 'on')
             or is_state('binary_sensor.bedroom_presence', 'on')
             or is_state('binary_sensor.kitchen_motion', 'on')
             or is_state('binary_sensor.hallway_presence', 'on') }}
        # 5-minute grace so a brief presence dropout doesn't
        # spin the camera up while you're still in the room.
        delay_off: "00:05:00"
# automations.yaml — privacy mode

# Engage privacy mode the moment anyone is home
- alias: "Privacy: Engage when someone home"
  description: "Wall-face the gimbal, kill recording + stream, disable tracking"
  mode: single
  trigger:
    - platform: state
      entity_id: binary_sensor.someone_home
      to: "on"
  action:
    # 1. Pivot the lens to the wall-facing privacy preset.
    #    For cameras without a hardware shutter, this is the
    #    primary privacy mechanism — make sure the preset is
    #    actually saved and points somewhere blind.
    - action: onvif.ptz
      target:
        entity_id: camera.ptz_main
      data:
        preset_token: "99"

    # 2. Hardware shutter (if your camera has one — skipped
    #    silently otherwise via continue_on_error)
    - action: switch.turn_on
      target:
        entity_id: switch.ptz_main_privacy_mode
      continue_on_error: true

    # 3. Stop recording at the camera. List every persistence
    #    switch your model exposes — recording, motion-triggered
    #    clips, audio capture, FTP upload, email alerts. Skip any
    #    switch your camera doesn't have via continue_on_error.
    - action: switch.turn_off
      target:
        entity_id:
          - switch.ptz_main_record
          - switch.ptz_main_motion_detection
          - switch.ptz_main_record_audio
          - switch.ptz_main_ftp_upload
          - switch.ptz_main_email
      continue_on_error: true

    # 4. Stop HA from pulling the RTSP stream. Kills any HA-side
    #    NVR / Frigate / LLAT recording. The camera itself is
    #    still up but no frames flow into HA.
    - action: camera.turn_off
      target:
        entity_id: camera.ptz_main

    # 5. Disable the Step 5 tracking automation so motion sensors
    #    can't trigger a snapshot pipeline (would fail anyway with
    #    the stream off, but no point spamming the log)
    - action: automation.turn_off
      target:
        entity_id: automation.ptz_track_motion_to_zone

    # 6. Confirm via push (one-time, no repeat) so you know it
    #    engaged. Comment out once you trust it.
    - action: notify.mobile_app_magikh0e_phone
      data:
        title: "🔒 Privacy mode"
        message: "Camera blinded — someone home"

# Disengage when the house has been empty for the grace window
- alias: "Privacy: Disengage when no one home"
  description: "Reverse all five privacy layers once everyone has left"
  mode: single
  trigger:
    - platform: state
      entity_id: binary_sensor.someone_home
      to: "off"
  action:
    # 1. Re-enable HA's stream pull first — the rest needs the
    #    camera entity to be available
    - action: camera.turn_on
      target:
        entity_id: camera.ptz_main

    # 2. Lift the hardware shutter
    - action: switch.turn_off
      target:
        entity_id: switch.ptz_main_privacy_mode
      continue_on_error: true

    # 3. Re-enable camera-side recording + alerts
    - action: switch.turn_on
      target:
        entity_id:
          - switch.ptz_main_record
          - switch.ptz_main_motion_detection
          - switch.ptz_main_record_audio
          - switch.ptz_main_ftp_upload
          - switch.ptz_main_email
      continue_on_error: true

    # 4. Park the gimbal at the home / idle preset
    - action: onvif.ptz
      target:
        entity_id: camera.ptz_main
      data:
        preset_token: "0"

    # 5. Re-enable Step 5 tracking
    - action: automation.turn_on
      target:
        entity_id: automation.ptz_track_motion_to_zone

    - action: notify.mobile_app_magikh0e_phone
      data:
        title: "🎥 Privacy mode lifted"
        message: "House is empty — surveillance back on"
Privacy caveats

  1. Phone GPS is not instant.
     Cellular GPS-based device_tracker updates can lag
     30–90 seconds when you walk in the door, especially
     in dense urban areas. The mmWave sensor backstop kicks
     privacy mode in fast even when GPS is still catching up.
     If you're paranoid, add a Wi-Fi-based zone trigger
     (router DHCP-lease integration) for instant detection.

  2. Hardware shutter beats software.
     A camera that physically closes its lens (Eufy, Reolink E1
     Outdoor) gives you cryptographic certainty the lens is
     blind. Software-only privacy modes are still trusting
     the firmware not to silently keep streaming.

  3. Test the privacy preset.
     Before trusting it, view the live stream while privacy mode
     is engaged and confirm the camera is actually pointed
     somewhere harmless. This matters most for no-shutter
     cameras — the preset IS your only privacy mechanism,
     and any drift past the wall edge means the lens is back on
     the room. Step 5's gimbal drives accumulate step-counter
     error over time. Re-save the preset every few months and
     spot-check the live stream as a routine.

  4. Guest mode.
     If a guest is over and you want surveillance off but their
     phone isn't tracked, the indoor sensors still flag
     someone_home and engage privacy mode. That's the
     right default. To toggle off manually for a long
     vacation while a house-sitter is over, expose
     input_boolean.privacy_override and OR it into the
     template above.

  5. Fail-safe orientation.
     If HA crashes mid-engage, the camera might be left in
     whatever state it was last commanded into. Decide whether
     your default-on-failure is "privacy preserved" or
     "surveillance running" and arrange the trigger
     directions accordingly. The example above prefers
     surveillance-on as the failure default (trigger on
     off restores the camera) — flip if you'd
     rather privacy-on be the default.

[ Optional — Auto-arm ]

A scheduled auto-arm at midnight, in case you forget. Fires only if the
panel is currently disarmed — won't disturb a manually-set state.
Pair this with a presence-based armed_away automation for full
hands-off coverage.
# Auto-arm at midnight if not already armed
- alias: "Alarm: Auto-arm at night"
  trigger:
    - platform: time
      at: "00:00:00"
  condition:
    - condition: state
      entity_id: alarm_control_panel.home_alarm
      state: disarmed
  action:
    - action: alarm_control_panel.alarm_arm_night
      target:
        entity_id: alarm_control_panel.home_alarm
      data:
        code: !secret alarm_code

[ ⚠ Caveats ]

Things to know before you trust this with your sleep schedule.

  1. This is not a monitored alarm.
     No central station, no police dispatch — just push notifications to
     your phone. For real security, layer this with a monitored service
     (SimpliSafe, Ring, etc.) or route notifications through a service
     you'll actually wake up for.

  2. Single point of failure.
     If HA is down, your network is dropped, or your phone is on
     do-not-disturb, the alarm doesn't work. Real systems carry cellular
     backup. This one carries hopes and dreams.

  3. Wi-Fi cameras drop offline.
     If your motion sensor is built into a Wi-Fi camera, it'll go
     unavailable periodically. Worth layering on a "device unavailable"
     alert so you know if a sensor stops reporting:
- alias: "Alarm: Sensor offline"
  trigger:
    - platform: state
      entity_id: binary_sensor.camera_e1_5295_motion_sensor
      to: "unavailable"
      for: "00:05:00"
  action:
    - action: notify.mobile_app_magikh0e_phone
      data:
        message: "Kitchen motion sensor has been offline for 5+ min"
  4. TTS provider — local Piper.
     The examples use tts.piper, a local neural TTS that runs
     in HA via the Wyoming protocol. Install it from Settings →
     Add-ons → Piper, then configure the integration so the
     tts.piper entity exists. Everything stays on-LAN — no
     internet round-trip per announcement, no subscription. If you'd
     rather use a hosted voice, swap to tts.home_assistant_cloud
     (Nabu Casa) or tts.google_translate_en_com (free, but
     phones home to Google for every utterance). The
     media_content_id URL prefix is the only thing that
     changes.

  5. No real siren.
     TTS through a Nest display + flashing lights is a deterrent — not a
     real deterrent. A Zigbee siren ($25 — Heiman, Frient, etc.)
     wired into the response automation is a serious upgrade. Add a
     switch.alarm_siren to the armed_triggered action and the
     burglar gets a 110 dB welcome instead of a friendly TTS voice.

  6. Code in plaintext.
     !secret alarm_code reads from secrets.yaml — that file
     belongs in your local backup workflow. Don't commit it to git, don't
     paste it into screenshots. The code only protects against casual
     dashboard access; anyone with shell on your HA box can read it.

[ See Also ]