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