#!/usr/bin/perl -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\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=>$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' }; } } 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) ) { 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" ); } } 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 == 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 ); } } } Info( "Got event - ".$event->as_string()."\n" ); } } 1;