Managing Zoneminder notifications with Home Assistant

The last few posts have described how I got Zoneminder working with object detection and configured so that notifications are managed by Home Assistant.

To recap, so far I’ve written about:

That left one minor issue to deal with – repeat notifications during ongoing motion events. It’s quite common for someone moving around to generate several separate motion events – they only have to stop for a couple of seconds for one event to end and another to be triggered when they next move. This can be mitigated to an extent by increasing the post-event buffer in Zoneminder, but I wanted to implement a throttle so that once a notification was sent I didn’t get additional notifications for the same camera for a defined period of time.

It turned out to be an easy task for AppDaemon. AppDaemon allows you to write automations in Python, based on listening for state changes and events within Home Assistant. Those scripts can then interact with Home Assistant by calling services such as the notification service.

The Python classes that you write are initialised when AppDaemon starts and are then persistent, making it easy to keep complex state data in standard Python variables. They also reinitialise automatically whenever the source code is changed, which is really handy when you’re developing them.

First of all I created a numeric input in HA to store the threshold and allow me to dynamically change it:

    min: 30
    max: 3600
    step: 1
    name: Zoneminder Notification Throttle
    mode: box

The time is stored in seconds, and I’ve chosen 600 – 10 minutes – as the threshold:

The next step was to set up a configuration file for my AppDaemon script that contained the entities that receive the alerts and their corresponding notification switches, as described in my last post.

I could have made this simpler by relying on the naming conventions and storing, for example, “back_garden” in the configuration and building the sensor and input names from that, but I felt it was better to make it explicit. I don’t like relying on naming conventions if I can avoid it.

I’m also storing the Zoneminder details – host and credentials – but that’s an optional extra to allow me to grab the image URL and include it in my notification.

  module: zoneminder
  class: ZoneMinder
  zmuser: !secret ZMUSER
  zmpass: !secret ZMPASS
  zmhost: !secret ZMHOST
  throttle: input_number.zm_notify_throttle
      notify: input_boolean.back_garden_notify
      notify: input_boolean.side_gate_notify
      notify: input_boolean.front_garden_notify

I then created the actual script.

The script works by listening for state changes to the sensors and storing the last time a notification was sent for each sensor. This is initially set to the minimum datetime value, so that the first event always triggers a notification.

If a new alert occurs it checks to see if notifications are turned on for the camera. It then checks to see if it has been more than the threshold time since the last notification, and if so it sends a notification and updates the last notification time.

The conversion of the threshold caught me out. It comes through from HA as the string “600.0” and for some reason Python doesn’t like converting that directly to an integer without converting to a float first, hence the double type conversion.

The state value is a string in JSON format, and will look something like this:

{"eventid":"26153","name":"Back Garden:(26153) [a] detected:person:100% Motion New,","monitor":"3","state":"alarm"}

The script converts it to a proper JSON object to extract the event ID. The “name” field is something of a misnomer – it contains the event details, including the camera name and other information that would probably be better split into separate fields.

I’m just grabbing the name of the camera, but it would be easy to extend it to pull out the confidence percentage of the motion detection to ignore events below a certain threshold. So far I haven’t needed to do that, as I’ve found that if the motion detection finds an object at all, even with low confidence, it’s usually correct.

Finally, I’m building up the URL of the alarm image and passing that to the pushover notification service. The service will grab the image from the URL and attach it to the notification, so I get a picture as well as the text. The resulting notification can be seen in the badly obfuscated image above.

NOTE: As of HA 0.106 it’s no longer possible to directly send an image from the URL. The workaround is to use the downloader component to download it first. I first set up a whitelist directory and the downloader component in configuration.yaml:

    - /config/www
    - /config/downloads

  download_dir: downloads

Then I added a step to download the file. My first attempt was to put the download call immediately before the notification:

self.call_service('downloader/download_file', url = url, filename = 'zonemindertemp.jpg', overwrite = True )
self.call_service('notify/pushover', message = detail, title = msg_title, data = { 'attachment': '/config/downloads/zonemindertemp.jpg' } )

That didn’t work. The downloader is asynchronous and there was no guarantee that the file had downloaded before the notification was sent. I therefore had to add a listener for the download complete event:

self.listen_event(self.downloaded, "downloader_download_completed", filename = 'zonemindertemp.jpg')

Within the state change callback, I then set the message title and details to be object level variables so that they were available in the download callback. I could then send my notification from the download_completed callback.

I also wanted to delete my temporary file when I was done. Since AppDaemon and Home Assistant were running in different Docker containers, I couldn’t simply delete the file directly from AppDaemon. I got around it by defining a shell command in my Home Assistant configuration.yaml to delete the file:

  delete_camera_snap: rm /config/downloads/zonemindertemp.jpg

I could then call the shell command as a service from within the AppDaemon script.

The full AppDaemon script is as follows:

import appdaemon.plugins.hass.hassapi as hass

import json
from datetime import datetime, timedelta

class ZoneMinder(hass.Hass):
    def initialize(self):

        self.last_alert = {}
        self.notify_entity = {}

        self.log('Zoneminder app initialising')

        if 'entities' in self.args:
            for entity in self.args['entities']:
                self.last_alert[entity] = datetime.min
                self.notify_entity[entity] = self.args['entities'][entity]['notify']
                self.listen_state(self.state_change, entity)
        self.listen_event(self.downloaded, "downloader_download_completed", filename = 'zonemindertemp.jpg')

    def downloaded(self, event_name, data, kwargs):

        self.call_service('notify/pushover', message = self.detail, title = self.msg_title, data = { 'attachment': '/config/downloads/zonemindertemp.jpg' } )

        self.detail = ''
        self.msg_title = ''

    def state_change(self, entity, attribute, old, new, kwargs):

        if self.get_state(self.notify_entity[entity]) == 'on':

            throttle = int(float(self.get_state(self.args['throttle'])))

            if self.datetime() > self.last_alert[entity] + timedelta(seconds=throttle):

                self.last_alert[entity] = self.datetime()

                state = json.loads(new)
                camera, self.detail = state['name'].split(':', 1)
                self.msg_title = 'Zoneminder alert on {}'.format(camera)

                url = "http://{}/zm/index.php?view=image&eid={}&fid=alarm&username={}&password={}".format(

                self.call_service('downloader/download_file', url = url, filename = 'zonemindertemp.jpg', overwrite = True )

This is my first AppDaemon script and it nicely completes my Zoneminder integration with Home Assistant, giving me full control of the notifications.

It’s been a fair amount of work – it’s easy to write it up when it’s all working, but there was a lot of new stuff to work out along the way. What I’ve got as a result, though, is worth the effort as it’s tailored exactly to my needs.

in Home Automation

Related Posts