#!/usr/bin/perl -wT
#
# ==========================================================================
#
# ZoneMinder External Trigger Script, $Date: 2008-07-25 10:48:16 +0100 (Fri, 25 Jul 2008) $, $Revision: 2612 $
# Copyright (C) 2001-2008 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 => 0; # 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;

# ==========================================================================
#
# Channel/Connection Modules
#
# ==========================================================================

use lib '/usr/local/lib/perl5/site_perl/5.12.4'; # Include custom perl install path
use ZoneMinder;
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=>ZM_PATH_SOCKS.'/zmtrigger.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 DBI;
#use Socket;
use Data::Dumper;
use POSIX qw( EINTR );
use Time::HiRes qw( usleep );

$| = 1;

$ENV{PATH}  = '/bin:/usr/bin:/usr/local/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 = zmDbConnect();

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 ) = zmMemRead( $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) || ($state == STATE_TAPE && $monitor->{LastState} != STATE_TAPE) ) # 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 )
	{
	    foreach my $monitor ( values(%monitors) )
        {
            # Free up any used memory handle
            zmMemInvalidate( $monitor );
        }
		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 ( !zmMemVerify( $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 ( !zmMemVerify( $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" );
	}
}
