#!@PERL_EXECUTABLE@ -wT # # ========================================================================== # # ZoneMinder X10 Control Script, $Date$, $Revision$ # 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # ========================================================================== =head1 NAME zmx10.pl - ZoneMinder X10 Control Script =head1 SYNOPSIS zmx10.pl -c ,--command= [-u ,--unit-code=] =head1 DESCRIPTION This script controls the monitoring of the X10 interface and the consequent management of the ZM daemons based on the receipt of X10 signals. =head1 OPTIONS -c , --command= - Command to issue, one of 'on','off','dim','bright','status','shutdown' -u , --unit-code= - Unit code to act on required for all commands except 'status' (optional) and 'shutdown' -v, --verison - Pirnts the currently installed version of ZoneMinder =cut use strict; use bytes; # ========================================================================== # # These are the elements you can edit to suit your installation # # ========================================================================== use constant CAUSE_STRING => 'X10'; # What gets written as the cause of any events # ========================================================================== # # Don't change anything below here # # ========================================================================== @EXTRA_PERL_LIB@ use ZoneMinder; use POSIX; use Socket; use Getopt::Long; use autouse 'Pod::Usage'=>qw(pod2usage); use autouse 'Data::Dumper'=>qw(Dumper); use constant SOCK_FILE => $Config{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)}; logInit(); logSetSignal(); my $command; my $unit_code; my $version; GetOptions( 'command=s' =>\$command, 'unit-code=i' =>\$unit_code, 'version' =>\$version ) or pod2usage(-exitstatus => -1); if ( $version ) { print ZoneMinder::Base::ZM_VERSION; exit(0); } 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); logReinit(); 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(); logReinit(); 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 = ) { 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 autouse 'Data::Dumper'=>qw(Dumper); our $dbh; our $x10; our %monitor_hash; our %device_hash; our %pending_tasks; sub runServer { Info('X10 server starting'); 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=>$Config{ZM_X10_DEVICE}, house_code=>$Config{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 = $Config{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 = ; my ($command, $unit_code) = split(';', $message); my $device; if ( defined($unit_code) ) { if ( $unit_code < 1 || $unit_code > 16 ) { dPrint(ZoneMinder::Logger::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' }; } } # end if defined($unit_code) 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(ZoneMinder::Logger::DEBUG, $unit_code.' '.$device->{status}."\n"); } else { foreach my $unit_code ( sort( keys(%device_hash) ) ) { my $device = $device_hash{$unit_code}; dPrint(ZoneMinder::Logger::DEBUG, $unit_code.' '.$device->{status}."\n"); } } } elsif ( $command eq 'shutdown' ) { last; } else { dPrint(ZoneMinder::Logger::ERROR, "Invalid command '$command'\n"); } if ( defined($result) ) { # FIXME if ( 1 || $result ) { $device->{status} = uc($command); dPrint(ZoneMinder::Logger::DEBUG, $device->{appliance}->address()." $command, ok\n"); #x10listen( new X10::Event( sprintf("%s %s", $device->{appliance}->address, uc($command) ) ) ); } else { dPrint(ZoneMinder::Logger::ERROR, $device->{appliance}->address()." $command, failed\n"); } } # end if defined result 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"); $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"); $task_list = $monitor->{OFF_list}; } if ( $task_list ) { foreach my $task ( @$task_list ) { processTask($task); } } } # end if defined laststate $monitor->{LastState} = $state; } # end foreach monitor # 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 or (++$reload_count >= $reload_limit) ) { loadTasks(); $reload = undef; $reload_count = 0; } } } Info("X10 server exiting"); 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" ); 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; } # end sub addToDeviceList 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" ); 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; } # end sub addToMonitorList sub loadTasks { %monitor_hash = (); Debug('Loading tasks'); # 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 M.Capturing != \'None\' 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() ) { # Check shared memory ok if ( !zmMemVerify($monitor) ) { zmMemInvalidate($monitor); next; } $monitor_hash{$monitor->{Id}} = $monitor; if ( $monitor->{Activation} ) { Debug("$monitor->{Name} has active string '$monitor->{Activation}'"); 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 ); } } # end if unit_code } # end foreach code_string } if ( $monitor->{AlarmInput} ) { Debug("$monitor->{Name} has alarm input string '$monitor->{AlarmInput}'"); 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 ); } } # end if unit_code } # end foreach code_string } # end if AlarmInput if ( $monitor->{AlarmOutput} ) { Debug("$monitor->{Name} has alarm output string '$monitor->{AlarmOutput}'"); 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 ); } } # end if unit_code } # end foreach code_string } # end if AlarmOutput zmMemInvalidate($monitor); } } # end sub loadTasks 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; } } # end switch task->type } # end foreach pending_task if ( @$new_pending_list ) { $pending_tasks{$activation_time} = $new_pending_list; } else { delete $pending_tasks{$activation_time}; } } # end foreach 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; } # end sub addPendingTask 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}); } } # end switch class } 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 == ZoneMinder::Logger::DEBUG ) { Debug(@_); } elsif ( $dbg_level == ZoneMinder::Logger::INFO ) { Info(@_); } elsif ( $dbg_level == ZoneMinder::Logger::WARNING ) { Warning(@_); } elsif ( $dbg_level == ZoneMinder::Logger::ERROR ) { Error( @_ ); } elsif ( $dbg_level == ZoneMinder::Logger::FATAL ) { Fatal( @_ ); } } sub x10listen { foreach my $event ( @_ ) { #print( Data::Dumper( $_ )."\n" ); if ( $event->house_code() eq $Config{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); } } } # end if correct house code Info('Got event - '.$event->as_string()); } } # end sub x10listen 1; __END__