Esphome alarm system

This needs much testing. I have my unit on a battery backup. It seems to work fine over multiple days which I can observe multiple disconnects from ha. Simple connectivity disconnects should be no problem since all logic is on the esp. But reboots haven’t been tested fully. The most sensitive portion is the trigger sequence. It is unlikely to recover from a reboot in the middle of a trigger sequence.

Thank you very much, that did the trick

1 Like

Interesting. Let me know if this start up step is required to start using it. I need to put that into my script, if so.

I just manually power cycled the device and observed the same behavior, where the home assistant alarm is locked. I have a physical arming mechanism which allows this behavior to be bypassed, ie I can still arm it the first time, then everything works.

A restore state mechanism needs to be built in with a default. I can’t find any information about a text sensor allowing a restore state. Only physical GPIO restore seems to be available. One option is to sepately store the armed/disarmed state in homeassistant as a back up restore mode. Still it would need to be decided how to handle the case when the esp cannot read this backup.

Do ‘normal’ security systems recover state upon total power off?

they can be graded out for a different reason… just saying.

There is an on_boot automation in esphome. My proposed working model is to use an input_sensor in homeassistant to store the last valid state from the esp alarm_condition. Then if this exists when the esp boots, use that as the initial state. If it doesn’t exist, set it to disarmed.

This will fail when power goes out to both the ha server and esp unit together as the esp unit will likely power on much faster. One could also put in a restore state mechanism in home assistant, but it gets tricky.

They would usually Tigger alarm

Are you saying that normal alarm systems turn on the siren every time the power goes out?

If power fails and battery dies, the external siren would alarm, it’s a fail safe in case someone cuts power or the wire to the bell.
The bells also have an internal battery.

I updated the gist but if you add

  on_boot:
    then:
      - script.execute: alarm_disarm

to the eshome block, the system will load up to be disarmed. This should fix the issue you had. I think another workaround is to call the esphome disarm service manually.

I’m working in the same type of wired system just right now. Thank you for all your coding yet. I’m just digging into it so I’ll be trying to post some code in a couple days.

I think I’ll go with the GPIO restoration mode for now. In my alarm system I have 2 spare pins (plus a couple more to implement I2C communication to the Rpi in the future) that can store up to 4 states if used as a binary number.

The key here is to have all the logic and automation in the ESP32 so I’ll give it a try.

P.S: Talking to my dad, he sugested using the EPROM to store the variable values, maybe with some kind of lambda in C++

I think the bottleneck is that a template text sensor doesn’t allow restore in esphome. The most important thing is probably to restore armed vs disarmed. We can use an internal esphome switch for this, which does support restoration, and use it to set the alarm condition on boot. I’ll try this next and report back.

I was pointed by a helpful person in the esphome discord to global variables, which does exactly what we want. I’m thinking that the global variable should be an integer and the keeper of the alarm state and the template text sensor should just map the integer state to be human/HA readable.

I need to think through the subtleties of when the system loses power/reboots during a trigger sequence. Some options: 1) The system returns as just ‘armed’, 2) The system returns as ‘pending’ and restarts the normal 10s (or whatever is chosen) countdown to become ‘triggered’, 3) store the actual time remaining in a different global variable and restore this and use it to restart the countdown. 3 is the most elegant, but hits the flash memory the hardest and there are only so many writes you can do to the flash memory on these devices. I will probably implement option 2.

I’ve been working in this the last couple days (thanks to Matthew’s code) and I’ve managed to retain states with a global int variable. I’d like to use a char type, but I had no luck yet. For now, this is my code (no automations nor binary sensors yet, just the retaining system). Every time I push the reset button in the NodeMCU, the state of the text sensor goes unavailable and then returns to previous state:

ESP HOME CONFIG

esphome:
  name: alarm_test
  platform: ESP8266
  board: nodemcuv2

  #Calls for restoration of the last alarm state
  on_boot: 
    then:
      - script.execute: restore

wifi:
  ssid: "XXXXXXXXXXXXXX"
  password: "XXXXXXXXXX"
  ap:
    ssid: "XXXXXXXX"
    password: "XXXXXXXXXX"

captive_portal:
logger:
ota:


# Global variable to store alarm state 
# 0=disarmed / 1=armed_home / 2=armed_away
globals:
  - id: state_int
    type: int
    restore_value: yes
    initial_value: '0'
    

# API services to arm and disarm alarm from Home Assistant
api:
  services:
    - service: arm_home_esp
      variables:
         code: string
      then:
        - if:
            condition:
              lambda: "return code == \"XXXX\";"
            then:
              - script.execute: arm_home_script
    - service: arm_away_esp
      variables:
         code: string
      then:
        - if:
            condition:
              lambda: "return code == \"XXXX\";"
            then:
              - script.execute: arm_away_script
    - service: disarm_esp
      variables:
         code: string
      then:
        - if:
            condition:
              lambda: "return code == \"XXXX\";"
            then:
              - script.execute: disarm_script


#Text sensor is slave to global variable
text_sensor:
  - platform: template
    name: "Alarm State"
    id: alarm_state


script:
  # restore values
  - id: restore
    then:
      - text_sensor.template.publish:
          id: alarm_state
          state: !lambda |-
            if (id(state_int) == 0) {
            return {"disarmed"};
            } else if (id(state_int) == 1){
            return {"armed_home"};
            } else if (id(state_int) == 2){
            return {"armed_away"};
            } else {
            return {"error"}; #added this just in case
            }

  # disarm 
  - id: disarm_script
    then:
      - lambda: !lambda |-
          id(state_int) = 0;
      - text_sensor.template.publish:
          id: alarm_state
          state: "disarmed"          

  # arm home 
  - id: arm_home_script
    then:
      - lambda: !lambda |-
          id(state_int) = 1;
      - text_sensor.template.publish:
          id: alarm_state
          state: "armed_home"

  # arm away 
  - id: arm_away_script
    then:
      - lambda: !lambda |-
          id(state_int) = 2;
      - text_sensor.template.publish:
          id: alarm_state
          state: "armed_away"

HOME ASSISTANT CONFIG

alarm_control_panel:

  - platform: template
    panels: 
      templalarma:
        name: Temlpalarma
        value_template: "{{ states('sensor.alarm_state') }}"
        arm_home:
          - condition: template
            value_template: "{{ code == 'XXXX' }}"
          - service: esphome.alarm_test_arm_home_esp    
            data:
              code: XXXX
        arm_away:
          - condition: template
            value_template: "{{ code == 'XXXX' }}"
          - service: esphome.alarm_test_arm_away_esp   
            data:
              code: XXXX
        disarm:
          - condition: template
            value_template: "{{ code == 'XXXX' }}"
          - service: esphome.alarm_test_disarm_esp      
            data:
              code: XXXX
1 Like

Thanks for starting this!

As I’ve found out the esp8266 uses the rtc memory by default for state restoration in esphome, so this won’t work over a power cycle. You can change it to use the flash which will survive a power cycle reboot. I’m told that the esp32 always writes to flash in esphome. But it may be a personal preference for each situation.

I’d prefer that it loads it to be disarmed in case of a restore error and have it log something. Unfortunately the template alarm panel in HA doesn’t recover well when you aren’t in an approved state which was the problem that @mistrovly encountered.

I’m testing with a NodeMCU on the bench, because my real alarm ESP32 is inside of the closet where I store my RPi, Ubuntu server, garden watering relays and router, among other automation stuff.

My alarm ESP32 and Rpi are connected to a battery which should provide a fair amount of time if the power goes out, but a software backup seems to be the safest way to go.

I will try to scale up to my real system, wich has 16 zones and a siren tamper soon and report.

What about using I2C expansion?

I’m using this logic, which shouldn’t require the on_boot automation at all, but might still be useful in case the restore functionality doesn’t work. Basically the on_boot automation would only check that the state_int value is between 0 and 3 and if not set it to 0. The below should be a little more compact for additional automations as you don’t have to push changes to both the global int and the template text sensor.

globals:
   - id: state_int
     type: int
     restore_value: yes
     initial_value: '0'

text_sensor:
  - platform: template
    name: "Alarm Condition"
    id: "alarm_condition"
    lambda: |-
      if (id(state_int) == 0) {
        return {"disarmed"};
      } else if (id(state_int) == 1){
        return {"pending"};
      } else if (id(state_int) == 2){
        return {"triggered"};
      } else if (id(state_int) == 3){
        return {"armed_home"};
      } else {
        return {"error"};
      }

script:
  - id: alarm_arm
    then:
      - lambda: |-
          id(state_int) = 3;
      - script.stop: trigger_alarm_execute
      - switch.turn_off: buzzer_front_door

  - id: alarm_disarm
    then:
      - lambda: |-
          id(state_int) = 0;
      - script.stop: trigger_alarm_execute
      - switch.turn_off: buzzer_front_door

  - id: trigger_alarm_execute
    then:
      - lambda: |-
          id(state_int) = 1;
      - switch.turn_on: buzzer_front_door
      - delay: 10s
      - lambda: |-
          id(state_int) = 2;
      - switch.turn_off: buzzer_front_door
      - delay: 3600s

  - id: trigger_alarm
    then:
      if:
        condition:
          and:
            - not:
                script.is_running: trigger_alarm_execute
            - lambda: |-
                return id(state_int) == 3;
        then:
            script.execute: trigger_alarm_execute

Once I fully test it I will update my link.

The pending state is a nice addition. What I don’t like about it is that the alarm will go into pending state no matter what door or window you open. Commercial alarms usually let you choose one or two doors that will wait those 10 seconds, but any other will trigger the siren right away.

My initial thought is to perform the pending state inside the “front door” binary switch. Something like this (not tested, just recycling your code):

binary_sensor:
  - platform: gpio
    pin:
      number: 19
      mode: INPUT_PULLUP
    name: "Front Door"
    device_class: door
    on_press:
      then:
        - lambda: |-
            id(state_int) = 1;
        - switch.turn_on: buzzer_front_door
        - delay: 10s
        - script.execute: trigger_alarm
   
1 Like