1#!@PERL_EXECUTABLE@ -wT 2# 3# ========================================================================== 4# 5# ZoneMinder X10 Control Script, $Date$, $Revision$ 6# Copyright (C) 2001-2008 Philip Coombes 7# 8# This program is free software; you can redistribute it and/or 9# modify it under the terms of the GNU General Public License 10# as published by the Free Software Foundation; either version 2 11# of the License, or (at your option) any later version. 12# 13# This program is distributed in the hope that it will be useful, 14# but WITHOUT ANY WARRANTY; without even the implied warranty of 15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16# GNU General Public License for more details. 17# 18# You should have received a copy of the GNU General Public License 19# along with this program; if not, write to the Free Software 20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 21# 22# ========================================================================== 23 24=head1 NAME 25 26zmx10.pl - ZoneMinder X10 Control Script 27 28=head1 SYNOPSIS 29 30 zmx10.pl -c <command>,--command=<command> [-u <unit code>,--unit-code=<unit code>] 31 32=head1 DESCRIPTION 33 34This script controls the monitoring of the X10 interface and the consequent 35management of the ZM daemons based on the receipt of X10 signals. 36 37=head1 OPTIONS 38 39 -c <command>, --command=<command> - Command to issue, one of 'on','off','dim','bright','status','shutdown' 40 -u <unit code>, --unit-code=<unit code> - Unit code to act on required for all commands 41 except 'status' (optional) and 'shutdown' 42 -v, --verison - Pirnts the currently installed version of ZoneMinder 43 44=cut 45use strict; 46use bytes; 47 48# ========================================================================== 49# 50# These are the elements you can edit to suit your installation 51# 52# ========================================================================== 53 54use constant CAUSE_STRING => 'X10'; # What gets written as the cause of any events 55 56# ========================================================================== 57# 58# Don't change anything below here 59# 60# ========================================================================== 61 62@EXTRA_PERL_LIB@ 63use ZoneMinder; 64use POSIX; 65use Socket; 66use Getopt::Long; 67use autouse 'Pod::Usage'=>qw(pod2usage); 68use autouse 'Data::Dumper'=>qw(Dumper); 69 70use constant SOCK_FILE => $Config{ZM_PATH_SOCKS}.'/zmx10.sock'; 71 72$| = 1; 73 74$ENV{PATH} = '/bin:/usr/bin:/usr/local/bin'; 75$ENV{SHELL} = '/bin/sh' if exists $ENV{SHELL}; 76delete @ENV{qw(IFS CDPATH ENV BASH_ENV)}; 77 78logInit(); 79logSetSignal(); 80 81my $command; 82my $unit_code; 83my $version; 84 85GetOptions( 86 'command=s' =>\$command, 87 'unit-code=i' =>\$unit_code, 88 'version' =>\$version 89) or pod2usage(-exitstatus => -1); 90 91if ( $version ) { 92 print ZoneMinder::Base::ZM_VERSION; 93 exit(0); 94} 95 96die 'No command given' unless $command; 97die 'No unit code given' 98unless( $unit_code || ($command =~ /(?:start|status|shutdown)/) ); 99 100if ( $command eq 'start' ) { 101 X10Server::runServer(); 102 exit(); 103} 104 105socket(CLIENT, PF_UNIX, SOCK_STREAM, 0) 106 or Fatal("Can't open socket: $!"); 107 108my $saddr = sockaddr_un(SOCK_FILE); 109 110if ( !connect(CLIENT, $saddr) ) { 111 # The server isn't there 112 print("Unable to connect, starting server\n"); 113 close(CLIENT); 114 115 if ( my $cpid = fork() ) { 116 # Parent process just sleep and fall through 117 sleep(2); 118 logReinit(); 119 socket(CLIENT, PF_UNIX, SOCK_STREAM, 0) 120 or Fatal("Can't open socket: $!"); 121 connect(CLIENT, $saddr) 122 or Fatal("Can't connect: $!"); 123 } elsif ( defined($cpid) ) { 124 setpgrp(); 125 126 logReinit(); 127 X10Server::runServer(); 128 } else { 129 Fatal("Can't fork: $!"); 130 } 131} 132# The server is there, connect to it 133#print( "Writing commands\n" ); 134CLIENT->autoflush(); 135my $message = $command; 136$message .= ';'.$unit_code if $unit_code; 137print(CLIENT $message); 138shutdown(CLIENT, 1); 139while ( my $line = <CLIENT> ) { 140 chomp($line); 141 print("$line\n"); 142} 143close(CLIENT); 144#print( "Finished writing, bye\n" ); 145exit; 146 147# 148# ========================================================================== 149# 150# This is the X10 Server package 151# 152# ========================================================================== 153# 154package X10Server; 155 156use strict; 157use bytes; 158 159use ZoneMinder; 160use POSIX; 161use DBI; 162use Socket; 163use X10::ActiveHome; 164use autouse 'Data::Dumper'=>qw(Dumper); 165 166our $dbh; 167our $x10; 168 169our %monitor_hash; 170our %device_hash; 171our %pending_tasks; 172 173sub runServer { 174 Info('X10 server starting'); 175 176 socket(SERVER, PF_UNIX, SOCK_STREAM, 0) 177 or Fatal("Can't open socket: $!"); 178 unlink(main::SOCK_FILE); 179 my $saddr = sockaddr_un(main::SOCK_FILE); 180 bind(SERVER, $saddr) or Fatal("Can't bind: $!"); 181 listen(SERVER, SOMAXCONN) or Fatal("Can't listen: $!"); 182 183 $dbh = zmDbConnect(); 184 185 $x10 = new X10::ActiveHome( 186 port=>$Config{ZM_X10_DEVICE}, 187 house_code=>$Config{ZM_X10_HOUSE_CODE}, 188 debug=>0 189 ); 190 191 loadTasks(); 192 193 $x10->register_listener(\&x10listen); 194 195 my $rin = ''; 196 vec($rin, fileno(SERVER),1) = 1; 197 vec($rin, $x10->select_fds(),1) = 1; 198 my $timeout = 0.2; 199 #print( 'F:'.fileno(SERVER)."\n" ); 200 my $reload = undef; 201 my $reload_count = 0; 202 my $reload_limit = $Config{ZM_X10_DB_RELOAD_INTERVAL} / $timeout; 203 while( 1 ) { 204 my $nfound = select(my $rout = $rin, undef, undef, $timeout); 205 #print( "Off select, NF:$nfound, ER:$!\n" ); 206 #print( vec( $rout, fileno(SERVER),1)."\n" ); 207 #print( vec( $rout, $x10->select_fds(),1)."\n" ); 208 if ( $nfound > 0 ) { 209 if ( vec($rout, fileno(SERVER),1) ) { 210 my $paddr = accept(CLIENT, SERVER); 211 my $message = <CLIENT>; 212 213 my ($command, $unit_code) = split(';', $message); 214 215 my $device; 216 if ( defined($unit_code) ) { 217 if ( $unit_code < 1 || $unit_code > 16 ) { 218 dPrint(ZoneMinder::Logger::ERROR, "Invalid unit code '$unit_code'\n"); 219 next; 220 } 221 222 $device = $device_hash{$unit_code}; 223 if ( !$device ) { 224 $device = $device_hash{$unit_code} = { 225 appliance=>$x10->Appliance(unit_code=>$unit_code), 226 status=>'unknown' 227 }; 228 } 229 } # end if defined($unit_code) 230 231 my $result; 232 if ( $command eq 'on' ) { 233 $result = $device->{appliance}->on(); 234 } elsif ( $command eq 'off' ) { 235 $result = $device->{appliance}->off(); 236 } 237 #elsif ( $command eq 'dim' ) 238 #{ 239 #$result = $device->{appliance}->dim(); 240 #} 241 #elsif ( $command eq 'bright' ) 242 #{ 243 #$result = $device->{appliance}->bright(); 244 #} 245 elsif ( $command eq 'status' ) { 246 if ( $device ) { 247 dPrint(ZoneMinder::Logger::DEBUG, $unit_code.' '.$device->{status}."\n"); 248 } else { 249 foreach my $unit_code ( sort( keys(%device_hash) ) ) { 250 my $device = $device_hash{$unit_code}; 251 dPrint(ZoneMinder::Logger::DEBUG, $unit_code.' '.$device->{status}."\n"); 252 } 253 } 254 } elsif ( $command eq 'shutdown' ) { 255 last; 256 } else { 257 dPrint(ZoneMinder::Logger::ERROR, "Invalid command '$command'\n"); 258 } 259 if ( defined($result) ) { 260 # FIXME 261 if ( 1 || $result ) { 262 $device->{status} = uc($command); 263 dPrint(ZoneMinder::Logger::DEBUG, $device->{appliance}->address()." $command, ok\n"); 264 #x10listen( new X10::Event( sprintf("%s %s", $device->{appliance}->address, uc($command) ) ) ); 265 } else { 266 dPrint(ZoneMinder::Logger::ERROR, $device->{appliance}->address()." $command, failed\n"); 267 } 268 } # end if defined result 269 close(CLIENT); 270 } elsif ( vec($rout, $x10->select_fds(),1) ) { 271 $x10->handle_input(); 272 } else { 273 Fatal('Bogus descriptor'); 274 } 275 } elsif ( $nfound < 0 ) { 276 if ( $! != EINTR ) { 277 Fatal("Can't select: $!"); 278 } 279 } else { 280 #print( "Select timed out\n" ); 281 # Check for state changes 282 foreach my $monitor_id ( sort(keys(%monitor_hash) ) ) { 283 my $monitor = $monitor_hash{$monitor_id}; 284 my $state = zmGetMonitorState($monitor); 285 if ( !defined($state) ) { 286 $reload = !undef; 287 next; 288 } 289 if ( defined( $monitor->{LastState} ) ) { 290 my $task_list; 291 if ( ($state == STATE_ALARM || $state == STATE_ALERT) 292 && ($monitor->{LastState} == STATE_IDLE || $monitor->{LastState} == STATE_TAPE) 293 ) # Gone into alarm state 294 { 295 Debug("Applying ON_list for $monitor_id"); 296 $task_list = $monitor->{ON_list}; 297 } elsif ( ($state == STATE_IDLE && $monitor->{LastState} != STATE_IDLE) 298 || ($state == STATE_TAPE && $monitor->{LastState} != STATE_TAPE) 299 ) # Come out of alarm state 300 { 301 Debug("Applying OFF_list for $monitor_id"); 302 $task_list = $monitor->{OFF_list}; 303 } 304 if ( $task_list ) { 305 foreach my $task ( @$task_list ) { 306 processTask($task); 307 } 308 } 309 } # end if defined laststate 310 $monitor->{LastState} = $state; 311 } # end foreach monitor 312 313 # Check for pending tasks 314 my $now = time(); 315 foreach my $activation_time ( sort(keys(%pending_tasks) ) ) { 316 last if ( $activation_time > $now ); 317 my $pending_list = $pending_tasks{$activation_time}; 318 foreach my $task ( @$pending_list ) { 319 processTask($task); 320 } 321 delete $pending_tasks{$activation_time}; 322 } 323 if ( $reload or (++$reload_count >= $reload_limit) ) { 324 loadTasks(); 325 $reload = undef; 326 $reload_count = 0; 327 } 328 } 329 } 330 Info("X10 server exiting"); 331 close(SERVER); 332 exit(); 333} 334 335sub addToDeviceList { 336 my $unit_code = shift; 337 my $event = shift; 338 my $monitor = shift; 339 my $function = shift; 340 my $limit = shift; 341 342 Debug("Adding to device list, uc:$unit_code, ev:$event, mo:" 343 .$monitor->{Id}.", fu:$function, li:$limit" 344 ); 345 my $device = $device_hash{$unit_code}; 346 if ( !$device ) { 347 $device = $device_hash{$unit_code} = { 348 appliance=>$x10->Appliance(unit_code=>$unit_code), 349 status=>'unknown' 350 }; 351 } 352 353 my $task = { 354 type=>'device', 355 monitor=>$monitor, 356 address=>$device->{appliance}->address(), 357 function=>$function 358 }; 359 360 if ( $limit ) { 361 $task->{limit} = $limit 362 } 363 364 my $task_list = $device->{$event.'_list'}; 365 if ( !$task_list ) { 366 $task_list = $device->{$event.'_list'} = []; 367 } 368 push @$task_list, $task; 369} # end sub addToDeviceList 370 371sub addToMonitorList { 372 my $monitor = shift; 373 my $event = shift; 374 my $unit_code = shift; 375 my $function = shift; 376 my $limit = shift; 377 378 Debug("Adding to monitor list, uc:$unit_code, ev:$event, mo:".$monitor->{Id} 379 .", fu:$function, li:$limit" 380 ); 381 my $device = $device_hash{$unit_code}; 382 if ( !$device ) { 383 $device = $device_hash{$unit_code} = { 384 appliance=>$x10->Appliance(unit_code=>$unit_code), 385 status=>'unknown' 386 }; 387 } 388 389 my $task = { 390 type=>'monitor', 391 device=>$device, 392 id=>$monitor->{Id}, 393 function=>$function 394 }; 395 if ( $limit ) { 396 $task->{limit} = $limit; 397 } 398 399 my $task_list = $monitor->{$event.'_list'}; 400 if ( !$task_list ) { 401 $task_list = $monitor->{$event.'_list'} = []; 402 } 403 push @$task_list, $task; 404} # end sub addToMonitorList 405 406sub loadTasks { 407 %monitor_hash = (); 408 409 Debug('Loading tasks'); 410 # Clear out all old device task lists 411 foreach my $unit_code ( sort keys(%device_hash) ) { 412 my $device = $device_hash{$unit_code}; 413 $device->{ON_list} = []; 414 $device->{OFF_list} = []; 415 } 416 417 my $sql = 'SELECT M.*,T.* FROM Monitors as M 418 INNER JOIN TriggersX10 as T on (M.Id = T.MonitorId) 419 WHERE find_in_set(M.`Function`, \'Modect,Record,Mocord,Nodect\') 420 AND M.`Enabled` = 1 421 AND find_IN_set(\'X10\', M.Triggers)'; 422 my $sth = $dbh->prepare_cached( $sql ) 423 or Fatal("Can't prepare '$sql': ".$dbh->errstr()); 424 my $res = $sth->execute() 425 or Fatal("Can't execute: ".$sth->errstr()); 426 while( my $monitor = $sth->fetchrow_hashref() ) { 427 # Check shared memory ok 428 if ( !zmMemVerify($monitor) ) { 429 zmMemInvalidate($monitor); 430 next; 431 } 432 433 $monitor_hash{$monitor->{Id}} = $monitor; 434 435 if ( $monitor->{Activation} ) { 436 Debug("$monitor->{Name} has active string '$monitor->{Activation}'"); 437 foreach my $code_string ( split(',', $monitor->{Activation}) ) { 438 #Debug( "Code string: $code_string\n" ); 439 my ( $invert, $unit_code, $modifier, $limit ) 440 = ( $code_string =~ /^([!~])?(\d+)(?:([+-])(\d+)?)?$/ ); 441 $limit = 0 if !$limit; 442 if ( $unit_code ) { 443 if ( !$modifier || $modifier eq '+' ) { 444 addToDeviceList( $unit_code, 445 'ON', 446 $monitor, 447 (!$invert ? 'start_active' : 'stop_active'), 448 $limit 449 ); 450 } 451 if ( !$modifier || $modifier eq '-' ) { 452 addToDeviceList( $unit_code, 453 'OFF', 454 $monitor, 455 (!$invert ? 'stop_active' : 'start_active'), 456 $limit 457 ); 458 } 459 } # end if unit_code 460 } # end foreach code_string 461 } 462 if ( $monitor->{AlarmInput} ) { 463 Debug("$monitor->{Name} has alarm input string '$monitor->{AlarmInput}'"); 464 foreach my $code_string ( split(',', $monitor->{AlarmInput}) ) { 465 #Debug( "Code string: $code_string\n" ); 466 my ( $invert, $unit_code, $modifier, $limit ) 467 = ( $code_string =~ /^([!~])?(\d+)(?:([+-])(\d+)?)?$/ ); 468 $limit = 0 if !$limit; 469 if ( $unit_code ) { 470 if ( !$modifier || $modifier eq '+' ) { 471 addToDeviceList( $unit_code, 472 'ON', 473 $monitor, 474 (!$invert ? 'start_alarm' : 'stop_alarm'), 475 $limit 476 ); 477 } 478 if ( !$modifier || $modifier eq '-' ) { 479 addToDeviceList( $unit_code, 480 'OFF', 481 $monitor, 482 (!$invert ? 'stop_alarm' : 'start_alarm'), 483 $limit 484 ); 485 } 486 } # end if unit_code 487 } # end foreach code_string 488 } # end if AlarmInput 489 if ( $monitor->{AlarmOutput} ) { 490 Debug("$monitor->{Name} has alarm output string '$monitor->{AlarmOutput}'"); 491 foreach my $code_string ( split( ',', $monitor->{AlarmOutput} ) ) { 492 #Debug( "Code string: $code_string\n" ); 493 my ( $invert, $unit_code, $modifier, $limit ) 494 = ( $code_string =~ /^([!~])?(\d+)(?:([+-])(\d+)?)?$/ ); 495 $limit = 0 if !$limit; 496 if ( $unit_code ) { 497 if ( !$modifier || $modifier eq '+' ) { 498 addToMonitorList( $monitor, 499 'ON', 500 $unit_code, 501 (!$invert ? 'on' : 'off'), 502 $limit 503 ); 504 } 505 if ( !$modifier || $modifier eq '-' ) { 506 addToMonitorList( $monitor, 507 'OFF', 508 $unit_code, 509 (!$invert ? 'off' : 'on'), 510 $limit 511 ); 512 } 513 } # end if unit_code 514 } # end foreach code_string 515 } # end if AlarmOutput 516 zmMemInvalidate($monitor); 517 } 518} # end sub loadTasks 519 520sub addPendingTask { 521 my $task = shift; 522 523 # Check whether we are just extending a previous pending task 524 # and remove it if it's there 525 foreach my $activation_time ( sort keys(%pending_tasks) ) { 526 my $pending_list = $pending_tasks{$activation_time}; 527 my $new_pending_list = []; 528 foreach my $pending_task ( @$pending_list ) { 529 if ( $task->{type} ne $pending_task->{type} ) { 530 push( @$new_pending_list, $pending_task ) 531 } elsif ( $task->{type} eq 'device' ) { 532 if (( $task->{monitor}->{Id} != $pending_task->{monitor}->{Id} ) 533 || ( $task->{function} ne $pending_task->{function} )) 534 { 535 push @$new_pending_list, $pending_task; 536 } 537 } elsif ( $task->{type} eq 'monitor' ) { 538 if (( $task->{device}->{appliance}->unit_code() 539 != $pending_task->{device}->{appliance}->unit_code() 540 ) 541 || ( $task->{function} ne $pending_task->{function} ) 542 ) { 543 push @$new_pending_list, $pending_task; 544 } 545 } # end switch task->type 546 } # end foreach pending_task 547 548 if ( @$new_pending_list ) { 549 $pending_tasks{$activation_time} = $new_pending_list; 550 } else { 551 delete $pending_tasks{$activation_time}; 552 } 553 } # end foreach activation_time 554 555 my $end_time = time() + $task->{limit}; 556 my $pending_list = $pending_tasks{$end_time}; 557 if ( !$pending_list ) { 558 $pending_list = $pending_tasks{$end_time} = []; 559 } 560 my $pending_task; 561 if ( $task->{type} eq 'device' ) { 562 $pending_task = { 563 type=>$task->{type}, 564 monitor=>$task->{monitor}, 565 function=>$task->{function} 566 }; 567 $pending_task->{function} =~ s/start/stop/; 568 } elsif ( $task->{type} eq 'monitor' ) { 569 $pending_task = { 570 type=>$task->{type}, 571 device=>$task->{device}, 572 function=>$task->{function} 573 }; 574 $pending_task->{function} =~ s/on/off/; 575 } 576 push @$pending_list, $pending_task; 577} # end sub addPendingTask 578 579sub processTask { 580 my $task = shift; 581 582 if ( $task->{type} eq 'device' ) { 583 my ( $instruction, $class ) = ( $task->{function} =~ /^(.+)_(.+)$/ ); 584 585 if ( $class eq 'active' ) { 586 if ( $instruction eq 'start' ) { 587 zmMonitorEnable($task->{monitor}); 588 if ( $task->{limit} ) { 589 addPendingTask($task); 590 } 591 } elsif( $instruction eq 'stop' ) { 592 zmMonitorDisable($task->{monitor}); 593 } 594 } elsif( $class eq 'alarm' ) { 595 if ( $instruction eq 'start' ) { 596 zmTriggerEventOn( 597 $task->{monitor}, 598 0, 599 main::CAUSE_STRING, 600 $task->{address} 601 ); 602 if ( $task->{limit} ) { 603 addPendingTask($task); 604 } 605 } elsif( $instruction eq 'stop' ) { 606 zmTriggerEventCancel($task->{monitor}); 607 } 608 } # end switch class 609 } elsif( $task->{type} eq 'monitor' ) { 610 if ( $task->{function} eq 'on' ) { 611 $task->{device}->{appliance}->on(); 612 if ( $task->{limit} ) { 613 addPendingTask($task); 614 } 615 } elsif ( $task->{function} eq 'off' ) { 616 $task->{device}->{appliance}->off(); 617 } 618 } 619} 620 621sub dPrint { 622 my $dbg_level = shift; 623 if ( fileno(CLIENT) ) { 624 print CLIENT @_ 625 } 626 if ( $dbg_level == ZoneMinder::Logger::DEBUG ) { 627 Debug(@_); 628 } elsif ( $dbg_level == ZoneMinder::Logger::INFO ) { 629 Info(@_); 630 } elsif ( $dbg_level == ZoneMinder::Logger::WARNING ) { 631 Warning(@_); 632 } 633 elsif ( $dbg_level == ZoneMinder::Logger::ERROR ) { 634 Error( @_ ); 635 } elsif ( $dbg_level == ZoneMinder::Logger::FATAL ) { 636 Fatal( @_ ); 637 } 638} 639 640sub x10listen { 641 foreach my $event ( @_ ) { 642 #print( Data::Dumper( $_ )."\n" ); 643 if ( $event->house_code() eq $Config{ZM_X10_HOUSE_CODE} ) { 644 my $unit_code = $event->unit_code(); 645 my $device = $device_hash{$unit_code}; 646 if ( !$device ) { 647 $device = $device_hash{$unit_code} = { 648 appliance=>$x10->Appliance(unit_code=>$unit_code), 649 status=>'unknown' 650 }; 651 } 652 next if ( $event->func() !~ /(?:ON|OFF)/ ); 653 $device->{status} = $event->func(); 654 my $task_list = $device->{$event->func().'_list'}; 655 if ( $task_list ) { 656 foreach my $task ( @$task_list ) { 657 processTask($task); 658 } 659 } 660 } # end if correct house code 661 Info('Got event - '.$event->as_string()); 662 } 663} # end sub x10listen 664 6651; 666__END__ 667