Instant notification on alarm + machine learning object detection
Posted: Wed Nov 22, 2017 5:46 pm
UPDATE 1/31/19: I have ditched the Perl script below and switched to Python with the ZoneMinder API. I have also implemented object detection to eliminate false alerts. See post 12 for more information and a link to the code.
It's easy to set up filters to send SMS or email notifications. The problem is, if you want to get prompt notifications, you have to set all of your filters to run very frequently, causing unnecessary load on your server, and even then there will be a delay between when motion was detected and when the notification is sent.
I wrote a perl script to send real-time (or very close to it) notifications based on the FAQ entry:
http://zoneminder.readthedocs.io/en/lat ... s-an-alarm
and the script in utils/zm-alarm.pl. However, I had some problems running those examples, as some of the functions called seem to be not actually exported from the ZoneMinder perl module. This link took care of that:
viewtopic.php?t=21781.
This has been done before, but this version adds the code for sending an email or SMS and has some other helpful features. It loops over each monitor and checks whether it is in an alarm/alert/tape state. If so, it sends a message immediately, unless the monitor was also in alarm/alert/tape state during the previous iteration. This is checked every 3 seconds. Because that timeout can cause some alarms to go unnoticed while they are occurring, a message will also be sent if there is a new event available since the previous iteration (and the monitor was not in alarm/alert/tape previously). It also performs some checks to make sure ZoneMinder is running and that monitors are sending a signal. Timeouts and other things are configurable, but these settings have been working well for me to receive very prompt alerts when motion is detected. Please fill in toaddr and eventurl in the notify functions and the From and MailFrom fields in the sendMessage function as appropriate.
Please excuse any poor coding practices on my part (but let me know!) All the perl I know was learned yesterday while trying to create this script.
EDIT 12/2/2017: Please see posts below for improvements and additional tips.
It's easy to set up filters to send SMS or email notifications. The problem is, if you want to get prompt notifications, you have to set all of your filters to run very frequently, causing unnecessary load on your server, and even then there will be a delay between when motion was detected and when the notification is sent.
I wrote a perl script to send real-time (or very close to it) notifications based on the FAQ entry:
http://zoneminder.readthedocs.io/en/lat ... s-an-alarm
and the script in utils/zm-alarm.pl. However, I had some problems running those examples, as some of the functions called seem to be not actually exported from the ZoneMinder perl module. This link took care of that:
viewtopic.php?t=21781.
This has been done before, but this version adds the code for sending an email or SMS and has some other helpful features. It loops over each monitor and checks whether it is in an alarm/alert/tape state. If so, it sends a message immediately, unless the monitor was also in alarm/alert/tape state during the previous iteration. This is checked every 3 seconds. Because that timeout can cause some alarms to go unnoticed while they are occurring, a message will also be sent if there is a new event available since the previous iteration (and the monitor was not in alarm/alert/tape previously). It also performs some checks to make sure ZoneMinder is running and that monitors are sending a signal. Timeouts and other things are configurable, but these settings have been working well for me to receive very prompt alerts when motion is detected. Please fill in toaddr and eventurl in the notify functions and the From and MailFrom fields in the sendMessage function as appropriate.
Code: Select all
#!/usr/bin/perl -w
# This script has been adapted from utils/zm-alarm.pl to send a notification
# when a monitor has been in an alarm/alert/tape state within the last 3
# seconds. For reference, see:
# scripts/ZoneMinder/lib/ZoneMinder/Memory.pm
# and
# http://zoneminder.readthedocs.io/en/latest/faq.html#how-can-i-use-zoneminder-to-trigger-something-else-when-there-is-an-alarm
# and
# https://forums.zoneminder.com/viewtopic.php?t=21781
use strict;
use warnings;
use ZoneMinder;
use DBI;
require MIME::Entity;
use List::Util qw[min max];
$| = 1;
# Interval between monitor checking cycles
my $timeout = 3;
# Length of pause after sending a message (to reduce spam)
our $message_timeout = 1;
# Only send a new event message for a given monitor if it was last detected
# in alarm state more than this many cycles previously. Goal is to prevent
# duplicate messages about the same event. Should be >= 2.
my $alarm_overload_count = 2;
# Email parameters
our @toaddrs = (""); # Enter to address(es) here
our $fromaddr = ""; # Enter from address here
our $eventurl = ""; # Enter URL to events page here
my $driver = "mysql";
my $database = "zm";
my $user = "zmuser";
my $password = "zmpass";
my @monitors;
my $rebuild_monitors = 1;
while (1) {
sleep $timeout;
# Have to exit when ZoneMinder stops running to make sure mapped memory is freed
my $status = runCommand( "zmdc.pl check" );
if ( $status ne "running" ) {
Info( "zm_event_alert.pl is exiting" );
last;
}
# Initialize or re-initialize array of monitors
if ( $rebuild_monitors ) {
my $dbh = DBI->connect(
"DBI:$driver:$database",
$user, $password,
) or die $DBI::errstr;
my $sql = "select M.*, max(E.Id) as LastEventId from Monitors as M left join Events as E on M.Id = E.MonitorId where M.Function != 'None' group by (M.Id)";
my $sth = $dbh->prepare_cached( $sql ) or die( "Can't prepare '$sql': ".$dbh->errstr() );
my $res = $sth->execute() or die( "Can't execute '$sql': ".$sth->errstr() );
@monitors = ();
while ( my $monitor = $sth->fetchrow_hashref() ) {
$monitor->{LastAlarm} = $alarm_overload_count + 1;
$monitor->{LastEventId} = zmGetLastEvent( $monitor );
$monitor->{Signal} = 1;
push( @monitors, $monitor );
}
}
$rebuild_monitors = 0;
# Loop over monitors
foreach my $monitor ( @monitors ) {
if ( !zmMemVerify( $monitor ) ) {
$rebuild_monitors = 1;
next;
}
# Skip any monitor that is inactive
if ( !getMonitorActive( $monitor ) ) {
next;
}
# Skip any monitor that is not receiving a signal
my $signal = getMonitorSignal( $monitor );
if ( !$signal ) {
$monitor->{Signal} = $signal;
Info("$monitor->{Name} is not sending a signal.");
next;
}
# Send a message in alarm state if not during previous cycle
if ( myInAlarm( $monitor ) ) {
if ( $monitor->{LastAlarm} > 1 ) {
notifyInAlarm( $monitor->{Name} );
Info("Sent alarm message for $monitor->{Name}");
}
$monitor->{LastAlarm} = 1;
} else {
$monitor->{LastAlarm} = min( $monitor->{LastAlarm} + 1,
$alarm_overload_count + 1);
}
# Send a message if a new event is available, because alarms may
# not be caught while they are occurring. Try to filter out any
# events caused by signal loss. Don't send a message if monitor
# was in alarm state within last alarm_overload_count cycles.
my $last_event_id = zmGetLastEvent( $monitor );
my $time = localtime();
if ( $last_event_id != $monitor->{LastEventId} ) {
if ( $monitor->{Signal} and
( $monitor->{LastAlarm} > $alarm_overload_count ) ) {
notifyNewEvent( $monitor->{Name},
$last_event_id );
Info("Sent new event $last_event_id alert ".
"for $monitor->{Name}");
} elsif ( $monitor->{Signal} ) {
Info("$monitor->{Name} event overload count ".
"$monitor->{LastAlarm}");
}
$monitor->{LastEventId} = $last_event_id;
}
$monitor->{Signal} = $signal;
}
}
sub getMonitorActive {
my $monitor = shift;
return( zmMemRead( $monitor, 'shared_data:active' ) );
}
sub getMonitorSignal {
my $monitor = shift;
return( zmMemRead( $monitor, 'shared_data:signal' ) );
}
# Same as zmInAlarm from Memory.pm, but also counts STATE_TAPE as alarm state
sub myInAlarm {
my $monitor = shift;
my $state = zmGetMonitorState( $monitor );
return( $state == 2 || $state == 3 || $state == 4 );
}
sub notifyInAlarm {
my $monitor_name = shift;
my $subject = "ZoneMinder alarm";
my $message = "Monitor $monitor_name is in alarm state!\n".
"URL: $eventurl";
foreach my $toaddr ( @toaddrs ) {
sendMessage($toaddr, $subject, $message);
}
}
sub notifyNewEvent {
my $monitor_name = shift;
my $last_event_id = shift;
my $subject = "ZoneMinder alarm";
my $message = "New event $last_event_id for monitor $monitor_name.\n".
"URL: $eventurl";
foreach my $toaddr ( @toaddrs ) {
sendMessage($toaddr, $subject, $message);
}
}
sub sendMessage {
my $toaddr = shift;
my $subject = shift;
my $message = shift;
my $mail = MIME::Entity->build(
From => $fromaddr,
To => $toaddr,
Subject => $subject,
Type => (($message=~/<html>/)?'text/html':'text/plain'),
Data => $message
);
$mail->smtpsend( Host => "localhost",
MailFrom => $fromaddr,
);
sleep $message_timeout;
}
EDIT 12/2/2017: Please see posts below for improvements and additional tips.