### secUpdater.pl ### Shawn Boyle [shawn@sboyle.com] ### 2.21.2003 #use warnings; use integer; use FileHandle; use Win32; use Win32::Console; use Win32::OLE 'EVENTS'; use Getopt::Long; use XML::Simple; use Data::Dumper; ### Argument processing my $argConfigFile = Win32::GetCwd()."\\aceUpdater.xml"; my $Logging = ''; my $Debug = ''; my ($ConfigOnly, $Help); my $Verbosity = 0; Getopt::Long::Configure("prefix_pattern=--|-|\/"); GetOptions( 'config=s' => \$argConfigFile, 'debug' => \$Debug, 'log' => \$Logging, 'verbose+' => \$Verbosity, 'configonly' => \$ConfigOnly, 'help' => \$Help, ); unless ($argConfigFile =~ /\\/) { $argConfigFile = Win32::GetCwd()."\\".$argConfigFile; } ### Clear the console and start output $STDOUT = new Win32::Console(STD_OUTPUT_HANDLE); $STDOUT->Cls; $STDOUT->Cursor(-1, -1, -1, 0); # -- Print help message -- # if ($Help) { print < ACE Message Handler. Shawn Boyle [shawn\@sboyle.com] v2.0 - 2.21.2003 INFO ### Read in the config file open(TEST, $argConfigFile) or die "Could not open config file: $argConfigFile\n"; print "Reading config file: $argConfigFile...\n"; my @keyatts = ("index", "value"); my $config = XMLin( $argConfigFile, keyattr => \@keyatts, forcearray => [ACE, command] ); #, GroupTags => {X10Config => 'address'} ); ### debug output if ($Debug) { open(OUT, ">AUDebug.txt"); print OUT Dumper($config); close OUT; } ### Logging logFile( 'initialize' ) if $Logging; ### Initialize some variables $ACECOMpw = $config->{'ACECOMpw'}; # SECURITY Vars # $headerText = $config->{SecurityConfig}->{HeaderText}; $HVMacro = $config->{SecurityConfig}->{HVSecMacroHex}; $NumZones = scalar keys %{$config->{SecurityConfig}->{'position'}}; $SecurityDefault = $config->{SecurityConfig}->{'position'}->{Default} ? 'defined' : 'not defined'; $NumZones-- if $config->{SecurityConfig}->{'position'}->{Default}; # Initialize the status array for ($i = 0; $i < $NumZones; $i++) { @secLastStatus[$i] = -1; } # X-10 Vars # my %HCOffset = ( A =>0, E =>4, I =>8, M =>12, B =>1, F =>5, J =>9, N =>13, C =>2, G =>6, K =>10, O =>14, D =>3, H =>7, L =>11, P =>15, ); my @x10LastStatus = (-2) x 256; my $NumX10s = scalar keys %{$config->{X10Config}->{address}}; $X10Default = $config->{X10Config}->{address}->{Default} ? 'defined' : 'not defined'; $NumX10s-- if $config->{X10Config}->{address}->{Default}; ### Print Config for user verification $HVMacroDec = hex $HVMacro; print <{SecurityConfig}->{HeaderText} HV macro d$HVMacroDec will be called for security updates. There are $NumZones zones and partitions defined in your XML file, and the 'Default' command set is $SecurityDefault. [ X-10 ] There are $NumX10s addresses defined in your XML file, and the 'Default' command set is $X10Default. CONFIG #updateSec('SECUPDATE:131000000000'); die "Config processing finished.\n" if $ConfigOnly; ### Initialize ACE object $ace = new Win32::OLE ( "AceServer32.Application" ); $aceNote = new Win32::OLE ( "AceServer32.Notify" ); $objNote = $aceNote->NotifyClass; Win32::OLE->WithEvents( $objNote, \&AceEvent); ### Check once now checkNow(); ### And wait Win32::OLE->MessageLoop; ### ---------- End Execution ---------- ### ### ========== Event Handler ========== ### sub AceEvent { my ($obj, $evt, @arg) = @_; #$STDOUT->Cursor(0, $y+5, -1, -1); foreach $msg (@arg) { print " <".$msg->Value."\n" if $Verbosity == 2; logFile( 'in', $msg->Value, 'EH') if $Verbosity == 1; for ($msg->Value) { /$headerText/i and do { logFile( 'in', $_, 'EH') if $Verbosity == 0; updateSec($_); last; }; /X10(HC)?UPDATE:/i and do { logFile( 'in', $_, 'EH') if $Verbosity == 0; x10Update($_); last; }; /AU:Update/i and do { logFile( 'in', $_, 'EH') if $Verbosity == 0; checkNow(); last; }; /AU:die/i and do { logFile( 'in', $_, 'EH') if $Verbosity == 0; die "Die command received.\n"; last; }; } } } ### ---------- Event Handler ---------- ### ### ========== ACEexec ========== ### # Send a command to ACE and update the log file # sub ACEexec { $ace->Execute($_[0], $ACECOMpw); print " >ACE: $_[0]\n"; logFile( 'out', $_[0], 'AE') if $Logging; } ### ---------- ACEexec ---------- ### ### ========== Check Now ========== ### # Update the status for everything. Either we've just been started # or a new client has connected to ACE. # sub checkNow { logFile( 'status', "checkNow") if $Logging; ### Initialize the status array # X-10 for ($i = 0; $i < scalar keys %{$config->{'position'}}; $i++) { @lastStatus[$i] = -1; } @x10LastStatus = (-2) x 256; # Security for ($i = 0; $i < $NumZones; $i++) { @secLastStatus[$i] = -1; } # Request status ACEexec( "ACES:HV:,O".$HVMacro."00" ); # Security ACEexec( "ACES:HV:,G65" ); # X-10 } ### ---------- Check Now ---------- ### ### ========== X-10 Udapte ========== ### # Handles the X10[HC]Update message. Keeps track of the status # of each X-10 address and performs the commands in the XML file # when that status changes. Each element in the status array # 0-255 corresponds to an X-10 address as per the table in the # HV documentation. # Possible values are: -1 - neutral # 0 - off # 100 - on # 01-99 - dim level # sub x10Update { my ($statusMsg) = @_; chomp $statusMsg; logFile( 'status', "x10Update" ) if $Logging; print "Parsing light status: "; ### Put each byte from the status message into an array if ($statusMsg =~ /X10UPDATE:(.*)/) { @x10CurrentStatus = unpack "C256", $1; print "Full update...\n"; ### Convert each byte to a level value for ($i=0; $i<256; $i++) { $x10CurrentStatus[$i] = convertToLevel($x10CurrentStatus[$i]); } } elsif ($statusMsg =~ /X10HCUPDATE:(.*)/) { print "Housecode ["; @temp = unpack "C17", $1; $houseCode = sprintf "%c", $temp[0]; print $houseCode."] update...\n"; my ($i, $c); $c = 1; for ($i=$HCOffset{$houseCode}*16; $i<($HCOffset{$houseCode}+1)*16; $i++) { $x10CurrentStatus[$i] = convertToLevel($temp[$c]); $c++; } } ### Go through each light for ($i=0; $i<256; $i++) { if ($x10CurrentStatus[$i] != $x10LastStatus[$i]) { # Convert to letter-number my $x10address = ('a'..'p')[int $i / 16]; $x10address .= ($i % 16) + 1; # Command to send #print $x10address.'['.$x10CurrentStatus[$i].':'.$x10LastStatus[$i]."]- "; for ($x10CurrentStatus[$i]) { $_ == -1 and do { $cmdToExecute = 'nuetral'; last; }; $_ == 0 and do { $cmdToExecute = 'off' ; last; }; $_ == 100 and do { $cmdToExecute = 'on' ; last; }; $cmdToExecute = 'dim' ; } ### Use the default command set if no other command is defined # If this address is defined but no commands are defined under it # copy in the default set if (($config->{X10Config}->{address}->{$x10address}) and not ($config->{X10Config}->{address}->{$x10address}->{command})) { $config->{X10Config}->{address}->{$x10address}->{command} = $config->{X10Config}->{address}->{'Default'}->{command}; } ### If Dim should default to on, replace the dim command with on if (($config->{X10Config}->{DimDefaultsToOn}->{value} =~ /yes/i) and (!$config->{X10Config}->{address}->{$x10address}->{command}->{'dim'}) and ($cmdToExecute eq 'dim')) { $cmdToExecute = 'on'; } ### Process ACE commands if ($config->{X10Config}->{address}->{$x10address}->{command}->{$cmdToExecute}->{ACE}) { # Print the status of the object printf "%-3s[L%-3s] %s:\n", $x10address, $x10CurrentStatus[$i], $cmdToExecute; my @commandsToSend; push @commandsToSend, @{$config->{X10Config}->{address}->{$x10address}->{command}->{$cmdToExecute}->{ACE}}; # For each command in the array perform attribute substitution # and execute for (@commandsToSend) { if (/default/i) { push @commandsToSend, @{$config->{X10Config}->{address}->{Default}->{command}->{$cmdToExecute}->{ACE}}; next; } my $cmd = $_; # Atribute substitution foreach $at (keys %{$config->{X10Config}->{address}->{$x10address}}) { next if $at eq 'command'; $$at = $config->{X10Config}->{address}->{$x10address}->{$at}; $cmd =~ s/\$$at/$$at/; } ACEexec( $cmd ); } } } } print "\n"; @x10LastStatus = @x10CurrentStatus; } ### ---------- X-10 Update ---------- ### ### ========== Update Security ========== ### # Handles the security update message. This is a user programed # message, the definition of which is stored in the XML file. # Keeps track of the status of each security object [partition, # zone...] and performs the commands in the XML file when that # status changes. # sub updateSec { my ($statusMsg) = @_; my (@currentStatus, $cmd); chomp $statusMsg; $statusMsg =~ s/\x01|\r|\f|\n//g; logFile( 'status', "updateSec" ); print "Parsing zone status:\n$statusMsg\n"; ### Store in a local var -- mostly to shorten for readability my $headerText = $config->{SecurityConfig}->{HeaderText}; $statusMsg =~ /:(\w*)$/i; $statusMsg = $1; my @temp = unpack "c$NumZones", $statusMsg; push @currentStatus, chr $_ foreach @temp; ### Send BTN commands for zones for ($pos = 0; $pos < $NumZones; $pos++) { ### If the status has changed if ($secLastStatus[$pos] != $currentStatus[$pos]) { ### Use the default command set if no other command is defined if (($config->{'SecurityConfig'}->{'position'}->{$pos}) and not ($config->{'SecurityConfig'}->{'position'}->{$pos}->{command})) { $config->{'SecurityConfig'}->{'position'}->{$pos}->{command} = $config->{'SecurityConfig'}->{'position'}->{'Default'}->{command}; } foreach $value (keys %{$config->{'SecurityConfig'}->{'position'}->{$pos}->{'command'}}) { if ($value == $currentStatus[$pos]) { # Execute all commands my @commandsToSend; push @commandsToSend, @{$config->{'SecurityConfig'}->{'position'}->{$pos}->{'command'}->{$value}->{ACE}}; foreach $_ (@commandsToSend) { if (/default/i) { push @commandsToSend, @{$config->{'SecurityConfig'}->{'position'}->{'Default'}->{command}->{$value}->{ACE}}; next; } my $cmd = $_; # Atribute substitution foreach $at (keys %{$config->{'SecurityConfig'}->{'position'}->{$pos}}) { next if $at eq 'command'; $$at = $config->{'SecurityConfig'}->{'position'}->{$pos}->{$at}; $cmd =~ s/\$$at/$$at/; } ACEexec( $cmd ); } } } } } @secLastStatus = @currentStatus; } ### ---------- Update Security ---------- ### ### ========== logFile ========== ### # Make an entry in the log file # sub logFile { my ($type, $msg, $from) = @_; my $statChr; ($sec,$min,$hour,$day,$mon,$year) = (localtime)[0..5]; $mon++; $year += 1900; my $dateStr = sprintf "%2.2d/%2.2d/%2.2d ", $mon,$day,$year; my $timeStr = sprintf "%2.2d:%2.2d:%2.2d ", $hour,$min,$sec; if ($type =~ /initialize/i) { open(LOG, ">AULog.txt"); autoflush LOG 1; print LOG "aceUpdater.exe Log File\n"; print LOG "Created: $dateStr $timeStr\n"; print LOG "Verbosity: $Verbosity\n\n"; } ### To keep the formatting aligned $from = ' ' unless $from; for ($type) { /status/i and do { $statChr = '|'; last; }; /error/i and do { $statChr = 'X'; last; }; /out/i and do { $statChr = '>'; last; }; /in/i and do { $statChr = '<'; last; }; $statChr = ' '; } ### Remove all the non-printable crap and whitespace from the message $msg =~ s/\x01|\r|\f|\n//g; print LOG "$timeStr"."[$from] $statChr $msg\n"; } ### ---------- logFile ---------- ### ### ========== Convert To Level ========== ### # Translated from the VB example in the CSI docs. [See: # "Auto Report Capabilities.pdf" for how this actually works] # Takes a status byte from an X-10 Update message and returns: # -1 - neutral # 0 - off # 100 - on # 01-99 - dim level # sub convertToLevel { my $byte = $_[0]; $byte -= 128; my $state = $byte % 4; if ($state == 0) { $level = 0; } elsif ($state == 1) { my $storedLevel = $byte / 4; if ($storedLevel == 0) { $level = 0; } elsif ($storedLevel == 16) { $level = 100; } else { no integer; my $regularLevel = $storedLevel % 16; my $tempResult = $regularLevel * 2; if ($storedLevel < 16) { $tempResult--; } $tempResult /= 0.3; $level = int $tempResult; } } else { $level = -1; } return ($level); } ### ---------- Convert To Level ---------- ###