#!/usr/pkg/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>,--command=<command> [-u <unit code>,--unit-code=<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=<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'
 -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
#
# ==========================================================================

# Include from system perl paths only
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}  = '/usr/pkg/bin:/bin:/usr/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 = <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 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 = <CLIENT>;

        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 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() ) {
    # 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__
