#!/usr/bin/perl -wT
#
# ==========================================================================
#
# ZoneMinder X10 Control Script, $Date: 2011-05-25 09:27:46 +0100 (Wed, 25 May 2011) $, $Revision: 3360 $
# 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 controls the monitoring of the X10 interface and the consequent
# management of the ZM daemons based on the receipt of X10 signals.
#
use strict;
use bytes;

# ==========================================================================
#
# These are the elements you can edit to suit your installation
#
# ==========================================================================

use constant DBG_ID => "zmx10"; # 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 CAUSE_STRING => "X10"; # What gets written as the cause of any events

# ==========================================================================
#
# Don't change anything below here
#
# ==========================================================================

use lib '/usr/local/lib/perl5/site_perl/5.12.4'; # Include custom perl install path
use ZoneMinder;
use POSIX;
use Socket;
use Getopt::Long;
use Data::Dumper;

use constant SOCK_FILE => ZM_PATH_SOCKS.'/zmx10.sock';

$| = 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)};

sub Usage
{
    print( "
Usage: zmx10.pl -c <command>,--command=<command> [-u <unit code>,--unit-code=<unit code>]
Parameters are :-
-c <command>, --command=<command>       - Command to issue, one of 'on','off','dim','bright','status','shutdown'
-u <unit code>, --unit-code=<unit code> - Unit code to act on required for all commands except 'status' (optional) and 'shutdown'
");
	exit( -1 );
}

zmDbgInit( DBG_ID, level=>DBG_LEVEL );
zmDbgSetSignal();

my $command;
my $unit_code;

if ( !GetOptions( 'command=s'=>\$command, 'unit-code=i'=>\$unit_code ) )
{
    Usage();
}

die( "No command given" ) unless( $command );
die( "No unit code given" ) unless( $unit_code || ($command =~ /(?:start|status|shutdown)/) );

if ( $command eq "start" )
{
	X10Server::runServer();
	exit();
}

socket( CLIENT, PF_UNIX, SOCK_STREAM, 0 ) or Fatal( "Can't open socket: $!" );

my $saddr = sockaddr_un( SOCK_FILE );

if ( !connect( CLIENT, $saddr ) )
{
	# The server isn't there 
	print( "Unable to connect, starting server\n" );
	close( CLIENT );

	if ( my $cpid = fork() )
	{
		# Parent process just sleep and fall through
		sleep( 2 );
		socket( CLIENT, PF_UNIX, SOCK_STREAM, 0 ) or Fatal( "Can't open socket: $!" );
		connect( CLIENT, $saddr ) or Fatal( "Can't connect: $!" );
	}
	elsif ( defined($cpid) )
	{
		setpgrp();

		X10Server::runServer();
	}
	else
	{
		Fatal( "Can't fork: $!" );
	}
}
# The server is there, connect to it
#print( "Writing commands\n" );
CLIENT->autoflush();
my $message = "$command";
$message .= ";$unit_code" if ( $unit_code );
print( CLIENT $message );
shutdown( CLIENT, 1 );
while ( my $line = <CLIENT> )
{
	chomp( $line );
	print( "$line\n" );
}
close( CLIENT );
#print( "Finished writing, bye\n" );
exit;

#
# ==========================================================================
#
# This is the X10 Server package
#
# ==========================================================================
#
package X10Server;

use strict;
use bytes;

use ZoneMinder;
use POSIX;
use DBI;
use Socket;
use X10::ActiveHome;
use Data::Dumper;

our $dbh;
our $x10;

our %monitor_hash;
our %device_hash;
our %pending_tasks;

sub runServer
{
	Info( "X10 server starting\n" );

	socket( SERVER, PF_UNIX, SOCK_STREAM, 0 ) or Fatal( "Can't open socket: $!" );
	unlink( main::SOCK_FILE );
	my $saddr = sockaddr_un( main::SOCK_FILE );
	bind( SERVER, $saddr ) or Fatal( "Can't bind: $!" );
	listen( SERVER, SOMAXCONN ) or Fatal( "Can't listen: $!" );

	$dbh = zmDbConnect();

	$x10 = new X10::ActiveHome( port=>ZM_X10_DEVICE, house_code=>ZM_X10_HOUSE_CODE, debug=>0 );

	loadTasks();

	$x10->register_listener( \&x10listen );

	my $rin = '';
	vec( $rin, fileno(SERVER),1) = 1;
	vec( $rin, $x10->select_fds(),1) = 1;
	my $timeout = 0.2;
	#print( "F:".fileno(SERVER)."\n" );
	my $reload = undef;
	my $reload_count = 0;
	my $reload_limit = &ZM_X10_DB_RELOAD_INTERVAL / $timeout;
	while( 1 )
	{
		my $nfound = select( my $rout = $rin, undef, undef, $timeout );
		#print( "Off select, NF:$nfound, ER:$!\n" );
		#print( vec( $rout, fileno(SERVER),1)."\n" );
		#print( vec( $rout, $x10->select_fds(),1)."\n" );
		if ( $nfound > 0 )
		{
			if ( vec( $rout, fileno(SERVER),1) )
			{
				my $paddr = accept( CLIENT, SERVER );
				my $message = <CLIENT>;

				my ( $command, $unit_code ) = split( ';', $message );

				my $device;
				if ( defined($unit_code) )
				{
					if ( $unit_code < 1 || $unit_code > 16 )
					{
						dPrint( DBG_ERROR, "Invalid unit code '$unit_code'\n" );
						next;
					}

					$device = $device_hash{$unit_code};
					if ( !$device )
					{
						$device = $device_hash{$unit_code} = { appliance=>$x10->Appliance( unit_code=>$unit_code ), status=>'unknown' };
					}
				}

				my $result;
				if ( $command eq 'on' )
				{
					$result = $device->{appliance}->on();
				}
				elsif ( $command eq 'off' )
				{
					$result = $device->{appliance}->off();
				}
				#elsif ( $command eq 'dim' )
				#{
					#$result = $device->{appliance}->dim();
				#}
				#elsif ( $command eq 'bright' )
				#{
					#$result = $device->{appliance}->bright();
				#}
				elsif ( $command eq 'status' )
				{
					if ( $device )
					{
						dPrint( DBG_DEBUG, $unit_code." ".$device->{status}."\n" );
					}
					else
					{
						foreach my $unit_code ( sort( keys(%device_hash) ) )
						{
							my $device = $device_hash{$unit_code};
							dPrint( DBG_DEBUG, $unit_code." ".$device->{status}."\n" );
						}
					}
				}
				elsif ( $command eq 'shutdown' )
				{
					last;
				}
				else
				{
					dPrint( DBG_ERROR, "Invalid command '$command'\n" );
				}
				if ( defined($result) )
				{
					if ( 1 || $result )
					{
						$device->{status} = uc($command);
						dPrint( DBG_DEBUG, $device->{appliance}->address()." $command, ok\n" );
						#x10listen( new X10::Event( sprintf("%s %s", $device->{appliance}->address, uc($command) ) ) );
					}
					else
					{
						dPrint( DBG_ERROR, $device->{appliance}->address()." $command, failed\n" );
					}
				}
				close( CLIENT );
			}
			elsif ( vec( $rout, $x10->select_fds(),1) )
			{
				$x10->handle_input();
			}
			else
			{
				Fatal( "Bogus descriptor" );
			}
		}
		elsif ( $nfound < 0 )
		{
			if ( $! != EINTR )
			{
				Fatal( "Can't select: $!" );
			}
		}
		else
		{
			#print( "Select timed out\n" );
			# Check for state changes
			foreach my $monitor_id ( sort(keys(%monitor_hash) ) )
			{
				my $monitor = $monitor_hash{$monitor_id};
				my $state = zmGetMonitorState( $monitor );
				if ( !defined($state) )
				{
					$reload = !undef;
					next;
				}
				if ( defined( $monitor->{LastState} ) )
				{
					my $task_list;
					if ( ($state == STATE_ALARM || $state == STATE_ALERT) && ($monitor->{LastState} == STATE_IDLE || $monitor->{LastState} == STATE_TAPE) ) # Gone into alarm state
					{
						Debug( "Applying ON_list for $monitor_id\n" );
						$task_list = $monitor->{"ON_list"};
					}
					elsif ( ($state == STATE_IDLE && $monitor->{LastState} != STATE_IDLE) || ($state == STATE_TAPE && $monitor->{LastState} != STATE_TAPE) ) # Come out of alarm state
					{
						Debug( "Applying OFF_list for $monitor_id\n" );
						$task_list = $monitor->{"OFF_list"};
					}
					if ( $task_list )
					{
						foreach my $task ( @$task_list )
						{
							processTask( $task );
						}
					}
				}
				$monitor->{LastState} = $state;
			}

			# Check for pending tasks
			my $now = time();
			foreach my $activation_time ( sort(keys(%pending_tasks) ) )
			{
				last if ( $activation_time > $now );
				my $pending_list = $pending_tasks{$activation_time};
				foreach my $task ( @$pending_list )
				{
					processTask( $task );
				}
				delete( $pending_tasks{$activation_time} );
			}
			if ( $reload || ++$reload_count >= $reload_limit )
			{
				loadTasks();
				$reload = undef;
				$reload_count = 0;
			}
		}
	}
	Info( "X10 server exiting\n" );
	close( SERVER );
	exit();
}

sub addToDeviceList
{
	my $unit_code = shift;
	my $event = shift;
	my $monitor = shift;
	my $function = shift;
	my $limit = shift;

	Debug( "Adding to device list, uc:$unit_code, ev:$event, mo:".$monitor->{Id}.", fu:$function, li:$limit\n" );
	my $device = $device_hash{$unit_code};
	if ( !$device )
	{
		$device = $device_hash{$unit_code} = { appliance=>$x10->Appliance( unit_code=>$unit_code ), status=>'unknown' };
	}

	my $task = { type=>"device", monitor=>$monitor, address=>$device->{appliance}->address(), function=>$function };
	if ( $limit )
	{
		$task->{limit} = $limit
	}

	my $task_list = $device->{$event."_list"};
	if ( !$task_list )
	{
		$task_list = $device->{$event."_list"} = [];
	}
	push( @$task_list, $task );
}

sub addToMonitorList
{
	my $monitor = shift;
	my $event = shift;
	my $unit_code = shift;
	my $function = shift;
	my $limit = shift;

	Debug( "Adding to monitor list, uc:$unit_code, ev:$event, mo:".$monitor->{Id}.", fu:$function, li:$limit\n" );
	my $device = $device_hash{$unit_code};
	if ( !$device )
	{
		$device = $device_hash{$unit_code} = { appliance=>$x10->Appliance( unit_code=>$unit_code ), status=>'unknown' };
	}

	my $task = { type=>"monitor", device=>$device, id=>$monitor->{Id}, function=>$function };
	if ( $limit )
	{
		$task->{limit} = $limit;
	}

	my $task_list = $monitor->{$event."_list"};
	if ( !$task_list )
	{
		$task_list = $monitor->{$event."_list"} = [];
	}
	push( @$task_list, $task );
}

sub loadTasks
{
	%monitor_hash = ();

	Debug( "Loading tasks\n" );
	# Clear out all old device task lists
	foreach my $unit_code ( sort( keys(%device_hash) ) )
	{
		my $device = $device_hash{$unit_code};
		$device->{ON_list} = [];
		$device->{OFF_list} = [];
	}

	my $sql = "select M.*,T.* from Monitors as M inner join TriggersX10 as T on (M.Id = T.MonitorId) where find_in_set( M.Function, 'Modect,Record,Mocord,Nodect' ) and M.Enabled = 1 and find_in_set( 'X10', M.Triggers )";
	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

		$monitor_hash{$monitor->{Id}} = $monitor;

		if ( $monitor->{Activation} )
		{
			Debug( "$monitor->{Name} has active string '$monitor->{Activation}'\n" );
			foreach my $code_string ( split( ',', $monitor->{Activation} ) )
			{
				#Debug( "Code string: $code_string\n" );
				my ( $invert, $unit_code, $modifier, $limit ) = ( $code_string =~ /^([!~])?(\d+)(?:([+-])(\d+)?)?$/ );
				$limit = 0 if ( !$limit );
				if ( $unit_code )
				{
					if ( !$modifier || $modifier eq '+' )
					{
						addToDeviceList( $unit_code, "ON", $monitor, !$invert?"start_active":"stop_active", $limit );
					}
					if ( !$modifier || $modifier eq '-' )
					{
						addToDeviceList( $unit_code, "OFF", $monitor, !$invert?"stop_active":"start_active", $limit );
					}
				}
			}
		}
		if ( $monitor->{AlarmInput} )
		{
			Debug( "$monitor->{Name} has alarm input string '$monitor->{AlarmInput}'\n" );
			foreach my $code_string ( split( ',', $monitor->{AlarmInput} ) )
			{
				#Debug( "Code string: $code_string\n" );
				my ( $invert, $unit_code, $modifier, $limit ) = ( $code_string =~ /^([!~])?(\d+)(?:([+-])(\d+)?)?$/ );
				$limit = 0 if ( !$limit );
				if ( $unit_code )
				{
					if ( !$modifier || $modifier eq '+' )
					{
						addToDeviceList( $unit_code, "ON", $monitor, !$invert?"start_alarm":"stop_alarm", $limit );
					}
					if ( !$modifier || $modifier eq '-' )
					{
						addToDeviceList( $unit_code, "OFF", $monitor, !$invert?"stop_alarm":"start_alarm", $limit );
					}
				}
			}
		}
		if ( $monitor->{AlarmOutput} )
		{
			Debug( "$monitor->{Name} has alarm output string '$monitor->{AlarmOutput}'\n" );
			foreach my $code_string ( split( ',', $monitor->{AlarmOutput} ) )
			{
				#Debug( "Code string: $code_string\n" );
				my ( $invert, $unit_code, $modifier, $limit ) = ( $code_string =~ /^([!~])?(\d+)(?:([+-])(\d+)?)?$/ );
				$limit = 0 if ( !$limit );
				if ( $unit_code )
				{
					if ( !$modifier || $modifier eq '+' )
					{
						addToMonitorList( $monitor, "ON", $unit_code, !$invert?"on":"off", $limit );
					}
					if ( !$modifier || $modifier eq '-' )
					{
						addToMonitorList( $monitor, "OFF", $unit_code, !$invert?"off":"on", $limit );
					}
				}
			}
		}
        zmMemInvalidate( $monitor );
	}
}

sub addPendingTask
{
	my $task = shift;

	# Check whether we are just extending a previous pending task
	# and remove it if it's there
	foreach my $activation_time ( sort(keys(%pending_tasks) ) )
	{
		my $pending_list = $pending_tasks{$activation_time};
		my $new_pending_list = [];
		foreach my $pending_task ( @$pending_list )
		{
			if ( $task->{type} ne $pending_task->{type} )
			{
				push( @$new_pending_list, $pending_task )
			}
			elsif ( $task->{type} eq "device" )
			{
				if (( $task->{monitor}->{Id} != $pending_task->{monitor}->{Id} )
				|| ( $task->{function} ne $pending_task->{function} ))
				{
					push( @$new_pending_list, $pending_task )
				}
			}
			elsif ( $task->{type} eq "monitor" )
			{
				if (( $task->{device}->{appliance}->unit_code() != $pending_task->{device}->{appliance}->unit_code() )
				|| ( $task->{function} ne $pending_task->{function} ))
				{
					push( @$new_pending_list, $pending_task )
				}
			}
		}
		if ( @$new_pending_list )
		{
			$pending_tasks{$activation_time} = $new_pending_list;
		}
		else
		{
			delete( $pending_tasks{$activation_time} );
		}
	}

	my $end_time = time() + $task->{limit};
	my $pending_list = $pending_tasks{$end_time};
	if ( !$pending_list )
	{
		$pending_list = $pending_tasks{$end_time} = [];
	}
	my $pending_task;
	if ( $task->{type} eq "device" )
	{
		$pending_task = { type=>$task->{type}, monitor=>$task->{monitor}, function=>$task->{function} };
		$pending_task->{function} =~ s/start/stop/;
	}
	elsif ( $task->{type} eq "monitor" )
	{
		$pending_task = { type=>$task->{type}, device=>$task->{device}, function=>$task->{function} };
		$pending_task->{function} =~ s/on/off/;
	}
	push( @$pending_list, $pending_task );
}

sub processTask
{
	my $task = shift;

	if ( $task->{type} eq "device" )
	{
		my ( $instruction, $class ) = ( $task->{function} =~ /^(.+)_(.+)$/ );

		if ( $class eq "active" )
		{
			if ( $instruction eq "start" )
			{
				zmMonitorEnable( $task->{monitor} );
				if ( $task->{limit} )
				{
					addPendingTask( $task );
				}
			}
			elsif( $instruction eq "stop" )
			{
				zmMonitorDisable( $task->{monitor} );
			}
		}
		elsif( $class eq "alarm" )
		{
			if ( $instruction eq "start" )
			{
				zmTriggerEventOn( $task->{monitor}, 0, main::CAUSE_STRING, $task->{address} );
				if ( $task->{limit} )
				{
					addPendingTask( $task );
				}
			}
			elsif( $instruction eq "stop" )
			{
				zmTriggerEventCancel( $task->{monitor} );
			}
		}
	}
	elsif( $task->{type} eq "monitor" )
	{
		if ( $task->{function} eq "on" )
		{
			$task->{device}->{appliance}->on();
			if ( $task->{limit} )
			{
				addPendingTask( $task );
			}
		}
		elsif ( $task->{function} eq "off" )
		{
			$task->{device}->{appliance}->off();
		}
	}
}

sub dPrint
{
	my $dbg_level = shift;
	if ( fileno(CLIENT) )
	{
		print CLIENT @_
	}
	if ( $dbg_level == DBG_DEBUG )
	{
		Debug( @_ );
	}
	elsif ( $dbg_level == DBG_INFO )
	{
		Info( @_ );
	}
	elsif ( $dbg_level == DBG_WARNING )
	{
		Warning( @_ );
	}
	elsif ( $dbg_level == DBG_ERROR )
	{
		Error( @_ );
	}
	elsif ( $dbg_level == DBG_FATAL )
	{
		Fatal( @_ );
	}
}

sub x10listen
{
	foreach my $event ( @_ )
	{
		#print( Data::Dumper( $_ )."\n" );
		if ( $event->house_code() eq ZM_X10_HOUSE_CODE )
		{
			my $unit_code = $event->unit_code();
			my $device = $device_hash{$unit_code};
			if ( !$device )
			{
				$device = $device_hash{$unit_code} = { appliance=>$x10->Appliance( unit_code=>$unit_code ), status=>'unknown' };
			}
			next if ( $event->func() !~ /(?:ON|OFF)/ );
			$device->{status} = $event->func();
			my $task_list = $device->{$event->func()."_list"};
			if ( $task_list )
			{
				foreach my $task ( @$task_list )
				{
					processTask( $task );
				}
			}
		}
		Info( "Got event - ".$event->as_string()."\n" );
	}
}

1;
