Security

Layered Home Security in Home Assistant: Contacts, Locks, Gates, and Alarmo

Independent technologist · 200+ HA devices · GriswoldLabs
10 min read

The thing nobody tells you about layered security in Home Assistant is that the layers will fight each other unless you build a single notification model on top of them. I have a RATGDO controller on the garage door, a custom ESP32 alarm on the side gate, two Z-Wave deadbolts, contact sensors on the front door, leak sensors plumbed into a shutoff valve, and Alarmo coordinating arming states. If I let each integration speak for itself, a single front-door-opened-while-armed event would generate four notifications inside ten seconds.

This post is how I get to one event = one notification, the YAML I use, and the failure modes I’ve hit.

The hardware layer

A quick inventory of what’s actually wired up:

  • Front door: Z-Wave deadbolt with keypad + Aqara contact sensor on the door itself
  • Garage interior door (laundry room): Z-Wave deadbolt with keypad
  • Garage door (the big one): RATGDO ESPHome controller for an existing chain-drive opener
  • Side gate: ESP32 with reed switch + battery monitor (my own build)
  • Back door: Aqara contact sensor + adjacent backyard light
  • Whole-home water shutoff: Z-Wave valve actuator on the main inlet
  • Leak sensors: Three Aqara water-leak sensors — under each sink, behind the washer

That’s seven separate “things that can trigger an event” and three response surfaces (notifications, arm/disarm states, and physical actions like shutoffs). Without a deliberate notification model, every change in any of those generates noise.

The notification model

The rule I settled on is: one notification surface per category of event, deduped by tag, with priority based on the event severity. Categories:

  • frontdoor-access — door open/close, optional toggle
  • lock-unlock-<lock> — keypad unlock by name (covered in the Z-Wave lock post)
  • gate-alert — side gate opened
  • garage-state — garage door opened / closed / left open
  • alarmo-state — armed / disarmed / triggered / failed to arm
  • leak-alert — water leak detected (highest priority — these are critical)
  • security-alert — failed HA login, suspicious activity

Each category uses a single notification tag. Repeated events of the same category replace the prior notification rather than stacking. This is the difference between “useful situational awareness” and “phone vibrating constantly when the front door is being used a lot.”

Front door + back door contacts

The simplest layer. Aqara contact sensor → binary sensor → notification. The package is built to be toggleable:

- id: '1748887175881'
  alias: Front Door Open and Close
  description: 'Notify when front door opens or closes'
  triggers:
    - trigger: state
      entity_id: binary_sensor.door_front_contact
  conditions:
    - condition: state
      entity_id: input_boolean.frontdoor_dooraccess_notifications
      state: 'on'
    - condition: template
      value_template: >
        {{ trigger.from_state.state in ('on', 'off') and
           trigger.to_state.state in ('on', 'off') }}
  actions:
    - action: notify.charles
      data:
        title: "Front Door"
        message: >
          {% if trigger.to_state.state == 'on' %}Door Opened{% else %}Door Closed{% endif %}
        data:
          tag: frontdoor-access

Three things worth noting:

  1. input_boolean.frontdoor_dooraccess_notifications is a manual mute. There are days where I’m working from home and the door is being opened constantly (deliveries, kids, dog) and the notifications become noise. One toggle on the dashboard kills them for the day.
  2. The state-validity template condition filters out transitions to unavailable or unknown. Without it, a brief Zigbee dropout would trigger “Front Door: Door Closed” because the from_state was on and the to_state was unavailable — which the message template renders as “closed” (since unavailable != 'on'). The condition makes sure both states are real before firing.
  3. tag: frontdoor-access dedupes — open then close in quick succession, you get one notification updated in place, not two.

The back door is similar but conditioned on mode: away only — I don’t care about back-door usage during the day at home, but if the back door opens while we’re out, that’s worth a notification.

The gate alarm (custom ESP32)

The side gate has a reed switch wired into a battery-powered ESP32 (covered in detail in the ESP32 round-up post). The ESP32 publishes the gate state via ESPHome native API. From HA’s side, it’s just binary_sensor.gate_alarm_side_gate:

- id: '1750622758112'
  alias: Gate is Open Notify
  description: 'Alert when side gate is opened'
  triggers:
    - trigger: state
      entity_id: binary_sensor.gate_alarm_side_gate
      to: 'on'
  conditions:
    # Don't alert during lawn service
    - condition: not
      conditions:
        - condition: state
          entity_id: calendar.lawn_guys
          state: 'on'
    # Cooldown - don't spam alerts
    - condition: template
      value_template: >
        {{ (as_timestamp(now()) - as_timestamp(state_attr('automation.gate_is_open_notify', 'last_triggered') or 0)) > 300 }}
  actions:
    - action: notify.family
      data:
        title: "Gate Alert"
        message: "The side gate is open"
        data:
          tag: gate-alert
          priority: high

Two clever bits here, both born of having actually run this for a year:

The calendar.lawn_guys condition is exactly what it sounds like. The lawn service comes on a recurring calendar schedule, opens the gate to bring mowers in, mows, leaves. I created an HA-native calendar called “Lawn Guys” and added their recurring weekly slot. When the calendar event is active, the gate-alert automation skips. When the schedule changes, I update the calendar instead of disabling the automation.

The same pattern works for any recurring authorized entry: cleaner, contractor, regular delivery. Build a calendar, condition the relevant security automations on it.

The five-minute cooldown uses last_triggered as a soft rate-limit. Without it, a windy day where the gate is bouncing on its hinges would generate fifty notifications in an hour. With it, max one alert per five minutes regardless of how many times the sensor changes.

Garage door (RATGDO)

The big garage door is controlled by a RATGDO ESPHome board — a small ESP32 that wires into the existing chain-drive opener and exposes it via HA as a cover entity. It’s the cleanest way I’ve found to put a chain-drive opener on Home Assistant without ripping out the existing setup. The board is about $50 and takes 10 minutes to install.

The automations on top of it cover three cases:

- id: 'garage_door_state_log'
  alias: Garage Door State Log
  triggers:
    - trigger: state
      entity_id: cover.ratgdov25_c8b3b1_door
  actions:
    - action: notify.charles
      data:
        title: "Garage Door"
        message: >
          {{ trigger.to_state.state | title }}
          {% if is_state('input_select.home_mode', 'Away') %}
          (you're not home)
          {% endif %}
        data:
          tag: garage-state

- id: 'garage_door_left_open_away_mode'
  alias: Garage Door Open in Away Mode
  triggers:
    - trigger: state
      entity_id: cover.ratgdov25_c8b3b1_door
      to: 'open'
      for:
        minutes: 5
  conditions:
    - condition: state
      entity_id: input_select.home_mode
      state: 'Away'
  actions:
    - action: notify.charles
      data:
        title: "⚠️ Garage Door Open"
        message: "Garage left open and you're not home"
        data:
          tag: garage-state-warning
          priority: high
    - action: cover.close_cover
      target:
        entity_id: cover.ratgdov25_c8b3b1_door

The first automation is informational — quietly tells me whenever the garage state changes, including a parenthetical “you’re not home” if mode is Away. The notification tag means stack-of-six-events becomes one notification updated in place.

The second is the consequential one. If the garage is open for five consecutive minutes and mode is Away, send a warning notification and close the door automatically. The automatic close is one of those features that sounds aggressive on paper and turns out to be useful in practice — kids come home, open the garage, walk into the house, forget the garage exists. Five minutes later it closes itself.

The auto-close has saved me from leaving the garage open overnight twice. It’s also closed the door on me once when I was walking back to the garage from the curb (eight minutes after I’d opened it, four minutes longer than the threshold). Adjusted my routine; haven’t had a problem since.

Alarmo: arming, triggers, failed-to-arm

Alarmo is the HA-native alarm panel integration. It coordinates a virtual armed/disarmed state and lets you wire any contact sensor as a “zone.” If a zone activates while armed, Alarmo enters a triggered state and you can attach actions to that.

I run Alarmo in a deliberately minimal way: it’s not driving the actual response (no siren, no local alarm), it’s driving notifications and lockdown automations. The zones are the front door, back door, side gate, and garage door. Arming uses HA’s native armed_home (some zones active) and armed_away (all zones active).

The notification automations:

- id: 'alarmo_triggered_notify_charles'
  alias: Alarmo Triggered - Notify Charles
  triggers:
    - trigger: state
      entity_id: alarm_control_panel.alarmo
      to: 'triggered'
  actions:
    - action: notify.charles
      data:
        title: "🚨 Alarm Triggered"
        message: "Alarmo is in triggered state — check entry sensors"
        data:
          tag: alarmo-state
          priority: high

- id: 'alarmo_failed_to_arm_notify_charles'
  alias: Alarmo Failed to Arm - Notify Charles
  triggers:
    - trigger: event
      event_type: alarmo_failed_to_arm
  actions:
    - action: notify.charles
      data:
        title: "Alarmo: Couldn't arm"
        message: >
          A sensor blocked arming —
          {% set sensors = trigger.event.data.sensors %}
          {% if sensors %}
          {{ sensors | map('regex_replace', '^binary_sensor\\.', '') | list | join(', ') }}
          {% else %}
          check Alarmo logs
          {% endif %}
        data:
          tag: alarmo-state

The failed_to_arm event is the one most setups skip. Without it, you arm “away” on the way out, a window is open, Alarmo silently refuses to arm, and you find out three hours later when you check that the house wasn’t actually armed. The event handler grabs the sensor list from trigger.event.data.sensors and tells you exactly which one was blocking. Makes the difference between a frustrating mystery and “oh, the laundry-room window.”

A small subtlety: the regex_replace strips the binary_sensor. prefix from the entity IDs so the notification reads “door_front_contact, window_laundry_contact” instead of the full prefixed names. Cosmetic but important — these notifications go straight to your phone lock screen.

Leak detection + automatic shutoff

The most consequential automation in the whole house, and the simplest:

- id: 'leak_detected_water_shutoff'
  alias: Leak Detected Water Shutoff
  description: 'Automatically shut off water supply when any leak sensor triggers'
  triggers:
    - trigger: state
      entity_id:
        - binary_sensor.leak_kitchen_sink
        - binary_sensor.leak_bathroom_sink
        - binary_sensor.leak_washer
      to: 'on'
  actions:
    - action: switch.turn_off
      target:
        entity_id: switch.water_main_valve
    - action: notify.family
      data:
        title: "🚨 LEAK DETECTED"
        message: >
          Water main shut off automatically.
          Leak sensor: {{ trigger.entity_id | replace('binary_sensor.leak_', '') | replace('_', ' ') | title }}
        data:
          tag: leak-alert
          priority: high

Anything triggers the leak sensor → main valve closes → notification to the whole family. No conditions, no debounce, no “are you sure” — these are the cheapest sensors in the house guarding against the most expensive failure mode I can think of (a slab leak running unattended for days). False positives cost me a couple hours of inconvenience to manually re-open the valve. Missing a real leak costs me thousands. The asymmetry is obvious.

I tested the system once a year by deliberately wetting a sensor with a paper towel. Both times, the valve closed in under three seconds and the notification landed before the valve finished closing.

What this all means for actual security

A few honest notes on what this setup actually buys me:

  • It’s surveillance, not deterrence. None of this stops a determined attacker. Z-Wave deadbolts are still locks; they’re as resistant to picking and bumping as any deadbolt. The cameras and sensors give me notification and after-the-fact logging. They don’t repel anyone.
  • It’s brittle if the network is down. Almost every layer here depends on HA being up and the home network being reachable. UPS on the HA host and the network gear is non-negotiable.
  • It generates real notifications I’d miss without it. Garage left open at night, gate flapping in the wind, water leak under the kitchen sink, a failed-to-arm condition that would have left the house unarmed. Each of those has fired at least once and would have cost me real time/money to discover the slow way.

If you take one piece of this for your own setup, take the leak shutoff. It’s the cheapest, the simplest, and the most consequential.

Tags: #home-assistant #alarmo #ratgdo #esp32 #security #z-wave
Share: X / Twitter Facebook

Related Articles