#!/usr/bin/perl -wT # # ========================================================================== # # ZoneMinder External Trigger Script, $Date$, $Revision$ # Copyright (C) 2003, 2004, 2005, 2006 Philip Coombes # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # ========================================================================== # # This script is used to trigger and cancel alarms from external connections # using an arbitrary text based format # use strict; use bytes; # ========================================================================== # # User config # # ========================================================================== use constant DBG_ID => "zmtrigger"; # Tag that appears in debug to identify source use constant DBG_LEVEL => 1; # 0 is errors, warnings and info only, > 0 for debug use constant MAX_CONNECT_DELAY => 10; use constant MONITOR_RELOAD_INTERVAL => 300; use constant SELECT_TIMEOUT => 0.25; use ZoneMinder::Trigger::Channel::Inet; use ZoneMinder::Trigger::Channel::Unix; use ZoneMinder::Trigger::Channel::Serial; use ZoneMinder::Trigger::Connection; my @connections; push( @connections, ZoneMinder::Trigger::Connection->new( name=>"Chan1", channel=>ZoneMinder::Trigger::Channel::Inet->new( port=>6802 ), mode=>"rw" ) ); push( @connections, ZoneMinder::Trigger::Connection->new( name=>"Chan2", channel=>ZoneMinder::Trigger::Channel::Unix->new( path=>'/tmp/test.sock' ), mode=>"rw" ) ); #push( @connections, ZoneMinder::Trigger::Connection->new( name=>"Chan3", channel=>ZoneMinder::Trigger::Channel::File->new( path=>'/tmp/zmtrigger.out' ), mode=>"w" ) ); push( @connections, ZoneMinder::Trigger::Connection->new( name=>"Chan4", channel=>ZoneMinder::Trigger::Channel::Serial->new( path=>'/dev/ttyS0' ), mode=>"rw" ) ); # ========================================================================== # # Don't change anything from here on down # # ========================================================================== use ZoneMinder; use DBI; #use Socket; use Data::Dumper; use POSIX qw( EINTR ); use Time::HiRes qw( usleep ); $| = 1; $ENV{PATH} = '/bin:/usr/bin'; $ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL}; delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; zmDbgInit( DBG_ID, level=>DBG_LEVEL ); zmDbgSetSignal(); Info( "Trigger daemon starting\n" ); my $dbh = DBI->connect( "DBI:mysql:database=".ZM_DB_NAME.";host=".ZM_DB_HOST, ZM_DB_USER, ZM_DB_PASS ); my $base_rin = ''; foreach my $connection ( @connections ) { Info( "Opening connection '$connection->{name}'\n" ); $connection->open(); } my @in_select_connections = grep { $_->input() && $_->selectable() } @connections; my @in_poll_connections = grep { $_->input() && !$_->selectable() } @connections; my @out_connections = grep { $_->output() } @connections; foreach my $connection ( @in_select_connections ) { vec( $base_rin, $connection->fileno(), 1 ) = 1; } my %spawned_connections; my %monitors; my $monitor_reload_time = 0; $! = undef; my $rin = ''; my $win = $rin; my $ein = $win; my $timeout = SELECT_TIMEOUT; my %actions; while( 1 ) { $rin = $base_rin; # Add the file descriptors of any spawned connections foreach my $fileno ( keys(%spawned_connections) ) { vec( $rin, $fileno, 1 ) = 1; } my $nfound = select( my $rout = $rin, undef, my $eout = $ein, $timeout ); if ( $nfound > 0 ) { Debug( "Got input from $nfound connections\n" ); foreach my $connection ( @in_select_connections ) { if ( vec( $rout, $connection->fileno(), 1 ) ) { Debug( "Got input from connection ".$connection->name()." (".$connection->fileno().")\n" ); if ( $connection->spawns() ) { my $new_connection = $connection->accept(); $spawned_connections{$new_connection->fileno()} = $new_connection; Debug( "Added new spawned connection (".$new_connection->fileno()."), ".int(keys(%spawned_connections))." spawned connections\n" ); } else { my $messages = $connection->getMessages(); if ( defined($messages) ) { foreach my $message ( @$messages ) { handleMessage( $connection, $message ); } } } } } foreach my $connection ( values(%spawned_connections) ) { if ( vec( $rout, $connection->fileno(), 1 ) ) { Debug( "Got input from spawned connection ".$connection->name()." (".$connection->fileno().")\n" ); my $messages = $connection->getMessages(); if ( defined($messages) ) { foreach my $message ( @$messages ) { handleMessage( $connection, $message ); } } else { delete( $spawned_connections{$connection->fileno()} ); Debug( "Removed spawned connection (".$connection->fileno()."), ".int(keys(%spawned_connections))." spawned connections\n" ); $connection->close(); } } } } elsif ( $nfound < 0 ) { if ( $! == EINTR ) { # Do nothing } else { Fatal( "Can't select: $!" ); } } # Check polled connections foreach my $connection ( @in_poll_connections ) { my $messages = $connection->getMessages(); if ( defined($messages) ) { foreach my $message ( @$messages ) { handleMessage( $connection, $message ); } } } # Check for alarms that might have happened my @out_messages; foreach my $monitor ( values(%monitors) ) { my ( $state, $last_event ) = zmShmRead( $monitor, [ "shared_data:state", "shared_data:last_event" ] ); #print( "$monitor->{Id}: S:$state, LE:$last_event\n" ); #print( "$monitor->{Id}: mS:$monitor->{LastState}, mLE:$monitor->{LastEvent}\n" ); if ( $state == STATE_ALARM || $state == STATE_ALERT ) # In alarm state { if ( !defined($monitor->{LastEvent}) || ($last_event != $monitor->{LastEvent}) ) # A new event { push( @out_messages, $monitor->{Id}."|on|".time()."|".$last_event ); } else # The same one as last time, so ignore it { # Do nothing } } elsif ( $state == STATE_IDLE && $monitor->{LastState} > STATE_IDLE ) # Out of alarm state { push( @out_messages, $monitor->{Id}."|off|".time()."|".$last_event ); } elsif ( defined($monitor->{LastEvent}) && ($last_event != $monitor->{LastEvent}) ) # We've missed a whole event { push( @out_messages, $monitor->{Id}."|on|".time()."|".$last_event ); push( @out_messages, $monitor->{Id}."|off|".time()."|".$last_event ); } $monitor->{LastState} = $state; $monitor->{LastEvent} = $last_event; } foreach my $connection ( @out_connections ) { if ( $connection->canWrite() ) { $connection->putMessages( \@out_messages ); } } foreach my $connection ( values(%spawned_connections) ) { if ( $connection->canWrite() ) { $connection->putMessages( \@out_messages ); } } Debug( "Checking for timed actions\n" ) if ( int(keys(%actions)) ); my $now = time(); foreach my $action_time ( sort( grep { $_ < $now } keys( %actions ) ) ) { Info( "Found actions expiring at $action_time\n" ); foreach my $action ( @{$actions{$action_time}} ) { my $connection = $action->{connection}; my $message = $action->{message}; Info( "Found action '$message'\n" ); handleMessage( $connection, $message ); } delete( $actions{$action_time} ); } # Allow connections to do their own timed actions foreach my $connection ( @connections ) { my $messages = $connection->timedActions(); if ( defined($messages) ) { foreach my $message ( @$messages ) { handleMessage( $connection, $message ); } } } foreach my $connection ( values(%spawned_connections) ) { my $messages = $connection->timedActions(); if ( defined($messages) ) { foreach my $message ( @$messages ) { handleMessage( $connection, $message ); } } } # If necessary reload monitors if ( (time() - $monitor_reload_time) > MONITOR_RELOAD_INTERVAL ) { loadMonitors(); } } Info( "Trigger daemon exiting\n" ); exit; sub loadMonitors { Debug( "Loading monitors\n" ); $monitor_reload_time = time(); my %new_monitors = (); my $sql = "select * from Monitors where find_in_set( Function, 'Modect,Mocord,Nodect' )"; my $sth = $dbh->prepare_cached( $sql ) or Fatal( "Can't prepare '$sql': ".$dbh->errstr() ); my $res = $sth->execute() or Fatal( "Can't execute: ".$sth->errstr() ); while( my $monitor = $sth->fetchrow_hashref() ) { next if ( !zmShmVerify( $monitor ) ); # Check shared memory ok if ( defined($monitors{$monitor->{Id}}->{LastState}) ) { $monitor->{LastState} = $monitors{$monitor->{Id}}->{LastState}; } else { $monitor->{LastState} = zmGetMonitorState( $monitor ); } if ( defined($monitors{$monitor->{Id}}->{LastEvent}) ) { $monitor->{LastEvent} = $monitors{$monitor->{Id}}->{LastEvent}; } else { $monitor->{LastEvent} = zmGetLastEvent( $monitor ); } $new_monitors{$monitor->{Id}} = $monitor; } %monitors = %new_monitors; } sub handleMessage { my $connection = shift; my $message = shift; my ( $id, $action, $score, $cause, $text, $showtext ) = split( /\|/, $message ); $score = 0 if ( !defined($score) ); $cause = "" if ( !defined($cause) ); $text = "" if ( !defined($text) ); my $monitor = $monitors{$id}; if ( !$monitor ) { Warning( "Can't find monitor '$id' for message '$message'\n" ); return; } Debug( "Found monitor for id '$id'\n" ); next if ( !zmShmVerify( $monitor ) ); Debug( "Handling action '$action'\n" ); if ( $action =~ /^(enable|disable)(?:\+(\d+))?$/ ) { my $state = $1; my $delay = $2; if ( $state eq "enable" ) { zmMonitorEnable( $monitor ); } else { zmMonitorDisable( $monitor ); } # Force a reload $monitor_reload_time = 0; Info( "Set monitor to $state\n" ); if ( $delay ) { my $action_time = time()+$delay; my $action_text = $id."|".(($state eq "enable")?"disable":"enable"); my $action_array = $actions{$action_time}; if ( !$action_array ) { $action_array = $actions{$action_time} = []; } push( @$action_array, { connection=>$connection, message=>$action_text } ); Debug( "Added timed event '$action_text', expires at $action_time (+$delay secs)\n" ); } } elsif ( $action =~ /^(on|off)(?:\+(\d+))?$/ ) { next if ( !$monitor->{Enabled} ); my $trigger = $1; my $delay = $2; my $trigger_data; if ( $trigger eq "on" ) { zmTriggerEventOn( $monitor, $score, $cause, $text ); zmTriggerShowtext( $monitor, $showtext ) if defined($showtext); Info( "Trigger '$trigger' '$cause'\n" ); } elsif ( $trigger eq "off" ) { my $last_event = zmGetLastEvent( $monitor ); zmTriggerEventOff( $monitor ); zmTriggerShowtext( $monitor, $showtext ) if defined($showtext); Info( "Trigger '$trigger'\n" ); # Wait til it's finished while( zmInAlarm( $monitor ) && ($last_event == zmGetLastEvent( $monitor )) ) { # Tenth of a second usleep( 100000 ); } zmTriggerEventCancel( $monitor ); } else { Info( "Trigger '$trigger'\n" ); zmTriggerEventCancel( $monitor ); } if ( $delay ) { my $action_time = time()+$delay; #my $action_text = $id."|cancel|0|".$cause."|".$text; my $action_text = $id."|cancel"; my $action_array = $actions{$action_time}; if ( !$action_array ) { $action_array = $actions{$action_time} = []; } push( @$action_array, { connection=>$connection, message=>$action_text } ); Debug( "Added timed event '$action_text', expires at $action_time (+$delay secs)\n" ); } } elsif( $action eq "cancel" ) { zmTriggerEventCancel( $monitor ); zmTriggerShowtext( $monitor, $showtext ) if defined($showtext); Info( "Cancelled event\n" ); } elsif( $action eq "show" ) { zmTriggerShowtext( $monitor, $showtext ); Info( "Updated show text to '$showtext'\n" ); } else { Error( "Unrecognised action '$action' in message '$message'\n" ); } }