package X10::ActiveHome; # Perl module for communicating with an X10 ActiveHome interface use Data::Dumper; use Device::SerialPort; use FileHandle; use IO::Select; use strict; use X10::Controller; use X10::Event; use vars qw( @ISA %hc_bits %uc_bits %fn_bits %reverse_bits ); @ISA = qw( X10::Controller ); %hc_bits = ( A => 0x60, B => 0xe0, C => 0x20, D => 0xa0, E => 0x10, F => 0x90, G => 0x50, H => 0xd0, I => 0x70, J => 0xf0, K => 0x30, L => 0xb0, M => 0x00, N => 0x80, O => 0x40, P => 0xc0, ); %uc_bits = ( 1 => 0x06, 2 => 0x0e, 3 => 0x02, 4 => 0x0a, 5 => 0x01, 6 => 0x09, 7 => 0x05, 8 => 0x0d, 9 => 0x07, 10 => 0x0f, 11 => 0x03, 12 => 0x0b, 13 => 0x00, 14 => 0x08, 15 => 0x04, 16 => 0x0c, ); %fn_bits = ( A0 => 0x00, # all off L1 => 0x01, # lights on ON => 0x02, # on OF => 0x03, # off DI => 0x04, # dim BR => 0x05, # bright L0 => 0x06, # lights off EC => 0x07, # extended code HR => 0x08, # hail request HA => 0x09, # hail acknowledge P1 => 0x0a, # preset dim 1 P2 => 0x0b, # preset dim 2 ED => 0x0c, # extended data S1 => 0x0d, # status on S0 => 0x0e, # status off SR => 0x0f, # status request ); %reverse_bits = ( 0 => [ 'M', 13, 'A0' ], 1 => [ 'E', 5, 'L1' ], 2 => [ 'C', 3, 'ON' ], 3 => [ 'K', 11, 'OF' ], 4 => [ 'O', 15, 'DI' ], 5 => [ 'G', 7, 'BR' ], 6 => [ 'A', 1, 'L0' ], 7 => [ 'I', 9, 'EC' ], 8 => [ 'N', 14, 'HR' ], 9 => [ 'F', 6, 'HA' ], 10 => [ 'D', 4, 'P1' ], 11 => [ 'L', 12, 'P2' ], 12 => [ 'P', 16, 'ED' ], 13 => [ 'H', 8, 'S1' ], 14 => [ 'B', 2, 'S0' ], 15 => [ 'J', 10, 'SR' ], ); ### constructors sub new { my $type = shift; my $self = new X10::Controller( @_ ); bless $self, $type; unless ($self->{test}) { return undef unless $self->{port}; $self->{serial_port} = new Device::SerialPort($self->{port}); $self->{serial_port}->baudrate(4800); $self->{serial_port}->databits(8); $self->{serial_port}->parity('none'); $self->{serial_port}->stopbits(1); $self->{serial_port}->handshake('none'); $self->{serial_port}->stty_echo(0); $self->{serial_port}->read_const_time(5000); $self->{serial_port}->read_char_time(5); } $self->{output_word_queue} = []; $self->{states} = {}; $self->{working} = 0; # used as a lock return $self; } ### public methods (most overriding parent class) sub select_fds { my $self = shift; if ($self->{test}) { return (); } else { return ($self->{serial_port}->{FD}); } } sub handle_input { my $self = shift; # get a byte my ($count, $byte) = $self->{serial_port}->read(1); return unless ($count); $byte = unpack("C", $byte); if ($byte == 0xa5) { $self->ah_settime; } elsif ($byte == 0x5a) { $self->get_ahwords; } elsif ($byte == 0x55) { $self->{logger}->('info', "Interface is ready") if $self->{debug}; $self->{is_ready} = 1; } else { $self->{logger}->('info', "Got 0x%02X -- not doing anything with it", $byte) if $self->{debug}; } } ### mostly-private methods... sub send_word { my $self = shift; my $word = uc(shift); my $good = 0; my $bad = 0; $self->{logger}->('info', "Sending: %s", $word) if $self->{debug}; my ($hc, $fn) = $word =~ /^(.)(..)$/; my $packet = ''; if ($fn eq 'DI' || $fn eq 'BR') { $self->{logger}->('info', "Sending: %s", $word) if $self->{verbose}; # this sends ONE DIM/BRIGHT with a Header byte of 0x16 $packet = pack("CC", 0x16, $hc_bits{$hc} | $fn_bits{$fn}); $self->ah_sendpacket($packet); } elsif ($fn > 0 && $fn <= 16) { $self->{logger}->('info', "Sending: %s", $word) if $self->{verbose}; # my $uc = $fn * 1; $packet = pack("CC", 0x04, ($hc_bits{$hc} | $uc_bits{$fn * 1})); $self->ah_sendpacket($packet); } # elsif ($fn eq 'ON' || $fn eq 'OF') elsif (exists $fn_bits{$fn}) { $self->{logger}->('info', "Sending: %s", $word) if $self->{verbose}; $packet = pack("CC", 0x06, $hc_bits{$hc} | $fn_bits{$fn}); $self->ah_sendpacket($packet); } else { $self->{logger}->('info', "Unsupported word: %s", $word); } } ### private methods -- don't call these from ouside! sub work { my $self = shift; return if $self->{working}; $self->{working} = 1; while (my $word = shift @{$self->{output_word_queue}}) { $self->send_word($word); $self->got_words($word); # AH interface doesn't feed back } $self->{working} = 0; } sub get_ahwords { my $self = shift; $self->{working} = 1; $self->{logger}->('info', "Sending 'gimme events' byte") if $self->{debug}; $self->{serial_port}->write(chr(0xc3)); my ($count, $bytes_to_read) = $self->{serial_port}->read(1); return 0 unless $count; $bytes_to_read = unpack("C", $bytes_to_read); return 0 if $bytes_to_read > 10; ($count, my $mask) = $self->{serial_port}->read(1); $mask = unpack("C", $mask); $bytes_to_read--; my @words = (); # foreach (1..$bytes_to_read) for (my $i = 0; $i < $bytes_to_read; $i++) { my $char; ($count, $char) = $self->{serial_port}->read(1); $char = unpack("C", $char); my $fabit = (1 << $i) & $mask; if ($fabit) # function { my $hc = $reverse_bits{($char & 0xf0) >> 4}->[0]; my $fn = $reverse_bits{$char & 0x0f}->[2]; push @words, sprintf("%1s%2s", $hc, $fn); if ($fn eq 'BR' || $fn eq 'DI') { # discard dim amount for now my ($j1, $j2) = $self->{serial_port}->read(1); $i++; } } else # address { my $hc = $reverse_bits{($char & 0xf0) >> 4}->[0]; my $uc = $reverse_bits{$char & 0x0f}->[1]; push @words, sprintf("%1s%02s", $hc, $uc); } } $self->got_words($self->optimize_wordlist(@words)) if @words; $self->{working} = 0; $self->work; } sub ah_settime { my $self = shift; $self->{working} = 1; my $sum_ok = 0; my $bad_sums = 0; my @time = localtime; my $seconds = $time[0]; my $minutes = $time[2] * 60 + $time[1]; my $hours = int($minutes / 12); $minutes %= 120; my $yday = $time[7]; my $wday = $time[6] % 7; $self->{logger}->('info', "Setting interface time/date...") if $self->{verbose}; $self->{serial_port}->write(chr(0x9b)); my $packet = pack("CCCCCC", 0, $minutes, $hours, $yday / 2, (($yday % 1) << 7) & (1 << $wday), 0x90, ); $self->ah_sendpacket($packet); $self->{working} = 0; $self->work; } sub ah_sendpacket { my $self = shift; my $packet = shift; my $checksum = 0; foreach (split(//, $packet)) { $checksum += ord($_); } $checksum &= 0xff; my $sum_ok = 0; my $bad_sums = 0; TRY: while ($bad_sums < 3 && !$sum_ok) { $self->{logger}->('info', "Sending %s-byte packet: %s", length($packet), join(", ", map { sprintf("0x%02X", ord($_)) } split(//, $packet) ), ) if $self->{debug}; $self->{serial_port}->write($packet); my ($count, $sum_from_device) = $self->{serial_port}->read(1); if ($count == 0) { $self->{logger}->('info', "Got nothing back from the device...") if $self->{debug}; $bad_sums++; } else { $sum_from_device = unpack("C", $sum_from_device); if ($checksum == $sum_from_device) { $self->{logger}->('info', "Good send, sending 0x00 as OK") if $self->{debug}; $self->{serial_port}->write(chr(0x00)); ($count, my $readybyte) = $self->{serial_port}->read(1); if ($count != 1) { $self->{logger}->('info', "Didn't get ready byte") if $self->{debug}; $bad_sums++; } elsif (ord($readybyte) != 0x55) { $self->{logger}->('info', "Expected ready 0x55, got 0x%02X", $readybyte) if $self->{debug}; $bad_sums++; } else { $self->{is_ready} = 1; $sum_ok = 1; } } else { $self->{logger}->('info', "Expected checksum 0x%02X, got 0x%02X", $checksum, $sum_from_device) if $self->{debug}; $bad_sums++; } } } if ($sum_ok) { $self->{logger}->('info', "Packet send successful") if $self->{debug}; } else { $self->{logger}->('info', "Aborting send: too many bad checksums") if $self->{debug}; } return $sum_ok; } sub ah_ring_signal { my $self = shift; my $bool = shift || 0; $self->ah_sendpacket(chr($bool ? 0xeb : 0xdb)); } sub DESTROY { my $self = shift; $self->{serial_port}->close; } ### utility functions -- not called as methods 1;